Consolidate suite into two tools; restore Rev 0 features

- Home page: two cards (SOP Configuration / Work Package Creator);
  SOP card turns green + "Review" once complete (persisted to localStorage),
  WP card locked until SOP is done
- Header: white background with dark text; remove duplicate title next to logo
- Move Step Comments toggle into the header; restore Usage Logs analytics
- Load Sample: project MICRON_PH1_CUP_HPM_FMCS INSTALL, PM Mariano Sanchez
- Team tab: add Assistant Project Manager + repeatable "Add Team Member"
- Sign-Offs: add Assistant Project Manager to additional roles
- Sequencing: restore Rev 0 pre-sequenced drag-and-drop (gates, arrows)
- Sources: restore Rev 0 pre-loaded list; defaults are deletable + addable
- WP Creator: replace broken stub with the real creator embedded in the WP
  tab, auto-loading the completed SOP from localStorage
- Remove duplicate standalone SOP tool (sop-config-*)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-11 14:57:53 -07:00
parent c583daae30
commit 2af4b58a9e
10 changed files with 484 additions and 1514 deletions

View File

@@ -167,6 +167,25 @@
.card-button:hover {
background: var(--cds-hover-primary);
}
/* COMPLETE STATE (SOP done) */
.card.complete {
background: #ecfdf5;
border-color: #16a34a;
}
.card.complete .card-button { background: #16a34a; }
.card.complete .card-button:hover { background: #15803d; }
.card-status {
display: inline-block;
font-size: 12px;
font-weight: 600;
color: #16a34a;
margin-bottom: 0.5rem;
}
.card.disabled {
opacity: 0.6;
pointer-events: none;
}
/* SECTION */
.section {
@@ -365,27 +384,19 @@
<!-- TOOL CARDS -->
<div class="cards-grid" id="overview">
<!-- UNIFIED SUITE -->
<a href="work-package-suite.html" class="card">
<div class="card-badge">Recommended</div>
<h3>Unified Suite</h3>
<p>Complete end-to-end workflow. Configure project SOP in 10 steps, then immediately create Work Packages with all SOP defaults pre-populated.</p>
<button class="card-button">Launch Suite</button>
</a>
<!-- SOP CONFIG TOOL -->
<a href="sop-config-tool.html" class="card">
<!-- SOP CONFIG -->
<a href="work-package-suite.html?tab=sop" class="card" id="card-sop">
<h3>SOP Configuration</h3>
<p>Standalone project baseline generator. Define roles, constraints, governance, and platforms. Generates exportable SOP JSON for sharing with team.</p>
<button class="card-button">Open Tool</button>
<p>Define the project baseline in 10 steps — team, sign-offs, WP types, governance, quality, platforms, sequence, constraints, and sources. Every Work Package inherits these defaults.</p>
<button class="card-button" id="card-sop-btn">Open Tool</button>
</a>
<!-- WP CREATION TOOL -->
<a href="wp-creation-index.html" class="card">
<!-- WP CREATOR -->
<a href="work-package-suite.html?tab=wp" class="card" id="card-wp">
<h3>Work Package Creator</h3>
<p>Individual Work Package authoring tool. Requires an SOP file. Create, review, and export individual IWPs with constraint checklists.</p>
<button class="card-button">Open Tool</button>
<p>Author individual Work Packages against the project SOP — pre-populated defaults, constraint checklists, and exportable IWPs. Complete the SOP first to unlock.</p>
<button class="card-button" id="card-wp-btn">Open Tool</button>
</a>
</div>
@@ -394,9 +405,9 @@
<div class="section quick-start">
<h2>Getting Started</h2>
<ol>
<li><strong>Click "Unified Suite"</strong> to begin</li>
<li><strong>Complete the 10-step SOP Configuration</strong> for your project (~15 minutes)</li>
<li><strong>Switch to "Work Package Creation" tab</strong> to start authoring Work Packages</li>
<li><strong>Open "SOP Configuration"</strong> and complete the 10 steps for your project (~15 minutes)</li>
<li><strong>Finish the SOP</strong> — this card turns green and unlocks the Work Package Creator</li>
<li><strong>Open "Work Package Creator"</strong> to author Work Packages with your SOP defaults pre-populated</li>
<li><strong>Leave feedback</strong> on any page using the feedback button below</li>
</ol>
</div>
@@ -426,8 +437,8 @@
<h2>About This Suite</h2>
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 1.5rem;">
<div>
<h3>Three Complementary Tools</h3>
<p>Unified workflow for end-to-end SOP+WP creation, standalone SOP generator, and individual WP authoring. Choose the approach that fits your workflow.</p>
<h3>Two-Step Workflow</h3>
<p>Configure the project SOP once, then author every Work Package against it. The Creator stays locked until the SOP is complete, so packages always inherit a valid baseline.</p>
</div>
<div>
<h3>Leave Feedback</h3>
@@ -448,8 +459,36 @@
</footer>
<script>
// Reflect SOP completion on the tool cards.
(function reflectSOPStatus(){
let complete = false, projName = '';
try {
complete = localStorage.getItem('wp_suite_sop_complete') === '1';
const sop = JSON.parse(localStorage.getItem('wp_suite_sop') || 'null');
projName = sop && sop.project && sop.project.name || '';
} catch(e){}
const sopCard = document.getElementById('card-sop');
const sopBtn = document.getElementById('card-sop-btn');
const wpCard = document.getElementById('card-wp');
const wpBtn = document.getElementById('card-wp-btn');
if(complete){
sopCard.classList.add('complete');
sopBtn.textContent = 'Review';
const status = document.createElement('div');
status.className = 'card-status';
status.textContent = '✓ SOP Complete' + (projName ? ' — ' + projName : '');
sopCard.insertBefore(status, sopCard.firstChild);
if(wpBtn) wpBtn.textContent = 'Open Creator';
} else {
if(wpCard) wpCard.classList.add('disabled');
if(wpBtn) wpBtn.textContent = 'Complete SOP first';
}
})();
let allComments = [];
function toggleComments() {
const panel = document.getElementById('comments-panel');
panel.classList.toggle('open');

View File

@@ -1,587 +0,0 @@
// ── 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.`);
}

View File

@@ -1,507 +0,0 @@
:root {
--primary: #2563eb;
--primary-light: #dbeafe;
--success: #16a34a;
--warning: #ea580c;
--danger: #dc2626;
--text: #1f2937;
--text-light: #6b7280;
--text-dim: #9ca3af;
--border: #e5e7eb;
--bg: #f9fafb;
--bg-card: #ffffff;
--shadow: 0 1px 3px rgba(0,0,0,0.1);
--shadow-lg: 0 10px 25px rgba(0,0,0,0.1);
}
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', sans-serif;
color: var(--text);
background: var(--bg);
line-height: 1.5;
}
.container {
max-width: 900px;
margin: 0 auto;
padding: 2rem 1rem;
}
/* HEADER */
.header {
background: linear-gradient(135deg, var(--primary) 0%, #1e40af 100%);
color: white;
padding: 1.5rem 2rem;
display: flex;
justify-content: space-between;
align-items: center;
box-shadow: var(--shadow-lg);
margin-bottom: 2rem;
border-radius: 0;
}
.header-left {
display: flex;
align-items: center;
gap: 1.5rem;
flex: 1;
}
.logo {
display: flex;
align-items: center;
gap: 0.5rem;
text-decoration: none;
color: white;
font-weight: 700;
font-size: 14px;
transition: opacity 0.2s;
}
.logo:hover { opacity: 0.9; }
.logo-icon {
width: 36px;
height: 36px;
background: rgba(255,255,255,0.2);
border-radius: 6px;
display: flex;
align-items: center;
justify-content: center;
font-size: 18px;
font-weight: 700;
}
.logo-text {
display: flex;
flex-direction: column;
line-height: 1;
font-size: 12px;
}
.header-title {
font-size: 20px;
font-weight: 700;
margin-bottom: 0.25rem;
color: white;
}
.header-subtitle {
font-size: 12px;
opacity: 0.85;
margin-bottom: 0;
color: white;
}
.header-right {
display: flex;
align-items: center;
gap: 1rem;
}
.header-button {
padding: 0.5rem 1rem;
background: rgba(255,255,255,0.2);
color: white;
border: 1px solid rgba(255,255,255,0.4);
border-radius: 6px;
cursor: pointer;
font-size: 13px;
font-weight: 600;
transition: all 0.2s;
}
.header-button:hover {
background: rgba(255,255,255,0.3);
border-color: white;
}
.header-meta { font-size: 12px; color: var(--text-dim); }
.step-counter {
background: rgba(255,255,255,0.2);
color: white;
padding: 0.4rem 0.8rem;
border-radius: 20px;
font-weight: 600;
font-size: 11px;
}
/* STEP NAVIGATION */
.step-nav {
margin-bottom: 2rem;
overflow-x: auto;
}
.steps-container {
display: flex;
gap: 0.5rem;
min-width: min-content;
padding: 0.5rem;
}
.step-item {
padding: 0.75rem 1rem;
border-radius: 6px;
background: var(--bg);
border: 2px solid var(--border);
cursor: pointer;
font-size: 12px;
font-weight: 600;
white-space: nowrap;
transition: all 0.2s;
}
.step-item:hover { background: var(--primary-light); border-color: var(--primary); }
.step-item.active {
background: var(--primary);
color: white;
border-color: var(--primary);
}
/* STEP CONTENT */
.step-content {
background: var(--bg-card);
padding: 2rem;
border-radius: 8px;
box-shadow: var(--shadow);
margin-bottom: 2rem;
}
.step { display: none; }
.step h2 {
font-size: 20px;
font-weight: 700;
margin-bottom: 0.5rem;
color: var(--text);
}
.notice {
font-size: 13px;
color: var(--text-light);
background: var(--primary-light);
padding: 0.75rem 1rem;
border-radius: 6px;
margin-bottom: 1.5rem;
border-left: 4px solid var(--primary);
}
/* FIELDS */
.field-grid {
display: grid;
gap: 1.5rem;
}
.field-grid.col1 { grid-template-columns: 1fr; }
.field {
display: flex;
flex-direction: column;
}
.field label {
font-size: 13px;
font-weight: 600;
margin-bottom: 0.5rem;
color: var(--text);
}
.field input,
.field select,
.field textarea {
padding: 0.75rem;
border: 1px solid var(--border);
border-radius: 6px;
font-size: 14px;
font-family: inherit;
color: var(--text);
background: var(--bg);
transition: border-color 0.2s;
}
.field input:focus,
.field select:focus,
.field textarea:focus {
outline: none;
border-color: var(--primary);
box-shadow: 0 0 0 3px var(--primary-light);
}
.field small {
font-size: 12px;
color: var(--text-dim);
margin-top: 0.25rem;
}
/* SUB-HEADING */
.sub-heading {
font-size: 14px;
font-weight: 700;
color: var(--text);
text-transform: uppercase;
letter-spacing: 0.05em;
}
/* REQUIRED ROLES */
.required-roles {
display: flex;
flex-direction: column;
gap: 1rem;
}
.role-required {
display: flex;
align-items: center;
padding: 1rem;
background: var(--bg);
border-radius: 6px;
border: 1px solid var(--border);
}
.role-checkbox {
display: flex;
align-items: center;
gap: 0.5rem;
}
.role-checkbox input[type="checkbox"] {
width: 18px;
height: 18px;
cursor: pointer;
}
.role-checkbox label {
margin: 0;
font-weight: 600;
cursor: pointer;
}
/* WP TYPES TABLE */
#wp-types-table {
overflow-x: auto;
}
.wp-type-row {
display: grid;
grid-template-columns: 30px 200px 200px 100px;
gap: 1rem;
align-items: center;
padding: 1rem;
background: var(--bg);
border-radius: 6px;
margin-bottom: 0.5rem;
border: 1px solid var(--border);
}
.wp-type-row input[type="checkbox"] {
width: 18px;
height: 18px;
cursor: pointer;
}
/* SEQUENCE */
.sequence-row {
display: grid;
grid-template-columns: 200px 100px 30px;
gap: 1rem;
align-items: center;
padding: 1rem;
background: var(--bg);
border-radius: 6px;
margin-bottom: 0.5rem;
border: 1px solid var(--border);
}
.sequence-row .row-del {
width: 30px;
height: 30px;
padding: 0;
background: var(--danger);
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
font-weight: 600;
}
.sequence-row .row-del:hover { background: #b91c1c; }
/* CONSTRAINTS */
.constraint-check {
display: flex;
align-items: center;
gap: 0.75rem;
padding: 0.75rem;
background: var(--bg);
border-radius: 6px;
border: 1px solid var(--border);
margin-bottom: 0.5rem;
}
.constraint-check input[type="checkbox"] {
width: 18px;
height: 18px;
cursor: pointer;
}
.constraint-check label {
margin: 0;
cursor: pointer;
font-size: 14px;
}
/* SOURCES */
.source-row {
display: grid;
grid-template-columns: 150px 150px 250px 150px 30px;
gap: 1rem;
align-items: center;
padding: 1rem;
background: var(--bg);
border-radius: 6px;
margin-bottom: 0.5rem;
border: 1px solid var(--border);
}
.source-row input,
.source-row select {
padding: 0.5rem;
font-size: 12px;
border: 1px solid var(--border);
border-radius: 4px;
}
.source-row .row-del {
width: 30px;
height: 30px;
padding: 0;
background: var(--danger);
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
font-weight: 600;
}
.source-row .row-del:hover { background: #b91c1c; }
/* BUTTONS */
.add-btn {
padding: 0.75rem 1.25rem;
background: var(--primary);
color: white;
border: none;
border-radius: 6px;
font-size: 14px;
font-weight: 600;
cursor: pointer;
transition: background 0.2s;
}
.add-btn:hover { background: #1d4ed8; }
.nav-btn {
padding: 0.75rem 1.5rem;
background: var(--bg);
border: 2px solid var(--border);
border-radius: 6px;
font-size: 14px;
font-weight: 600;
cursor: pointer;
transition: all 0.2s;
}
.nav-btn:hover {
border-color: var(--primary);
color: var(--primary);
}
.nav-btn.primary {
background: var(--primary);
color: white;
border-color: var(--primary);
}
.nav-btn.primary:hover {
background: #1d4ed8;
border-color: #1d4ed8;
}
.nav-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
/* NAVIGATION */
.step-navigation {
display: flex;
gap: 1rem;
justify-content: space-between;
padding: 1.5rem;
background: var(--bg-card);
border-radius: 8px;
box-shadow: var(--shadow);
}
/* MODAL */
.modal {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0,0,0,0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
}
.modal-content {
background: var(--bg-card);
border-radius: 8px;
padding: 2rem;
max-width: 600px;
max-height: 80vh;
overflow-y: auto;
box-shadow: var(--shadow-lg);
position: relative;
}
.modal-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1.5rem;
border-bottom: 1px solid var(--border);
padding-bottom: 1rem;
}
.modal-header h3 { font-size: 18px; margin: 0; }
.modal-close {
background: none;
border: none;
font-size: 20px;
cursor: pointer;
color: var(--text-dim);
}
.modal-close:hover { color: var(--text); }
.constraint-option {
padding: 0.75rem;
background: var(--bg);
border: 1px solid var(--border);
border-radius: 6px;
margin-bottom: 0.5rem;
cursor: pointer;
transition: all 0.2s;
}
.constraint-option:hover {
border-color: var(--primary);
background: var(--primary-light);
}
/* RESPONSIVE */
@media (max-width: 768px) {
.wp-type-row,
.source-row {
grid-template-columns: 1fr;
}
.step-navigation {
flex-direction: column;
}
}
/* COMMENTS SECTION */
.comments-section {
margin-top: 2rem;
padding-top: 1.5rem;
border-top: 2px solid var(--border);
}
.comments-toggle {
padding: 0.5rem 1rem;
background: var(--primary-light);
color: var(--primary);
border: 1px solid var(--primary);
border-radius: 6px;
font-size: 13px;
font-weight: 600;
cursor: pointer;
transition: all 0.2s;
}
.comments-toggle:hover {
background: var(--primary);
color: white;
}

View File

@@ -1,309 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Work Package SOP Configuration Tool</title>
<link rel="stylesheet" href="theme-light.css">
<link rel="stylesheet" href="sop-config-styles.css">
</head>
<body>
<div class="container">
<!-- HEADER -->
<div class="header">
<div class="header-left">
<a href="index.html" class="logo" title="Back to Home">
<img src="prime-controls-logo.jpg" alt="Prime Controls" style="height: 36px; width: auto;">
<div class="logo-text">SOP Configuration</div>
</a>
<div>
<div class="header-title">SOP Configuration Tool</div>
<div class="header-subtitle">Project-specific baseline parameters</div>
</div>
</div>
<div class="header-right">
<button class="header-button" onclick="loadSampleData()" title="Load example data">⭐ Load Sample</button>
<span class="step-counter"><span id="current-step">1</span> / 10</span>
</div>
</div>
<!-- STEP INDICATORS -->
<div class="step-nav">
<div class="steps-container">
<div class="step-item active" data-step="1" onclick="goToStep(1)">Project</div>
<div class="step-item" data-step="2" onclick="goToStep(2)">Team</div>
<div class="step-item" data-step="3" onclick="goToStep(3)">Sign-Offs</div>
<div class="step-item" data-step="4" onclick="goToStep(4)">WP Types</div>
<div class="step-item" data-step="5" onclick="goToStep(5)">Governance</div>
<div class="step-item" data-step="6" onclick="goToStep(6)">Quality</div>
<div class="step-item" data-step="7" onclick="goToStep(7)">Platforms</div>
<div class="step-item" data-step="8" onclick="goToStep(8)">Sequence</div>
<div class="step-item" data-step="9" onclick="goToStep(9)">Constraints</div>
<div class="step-item" data-step="10" onclick="goToStep(10)">Sources</div>
</div>
</div>
<!-- STEP CONTENT -->
<div class="step-content">
<!-- STEP 1: PROJECT BASICS -->
<div class="step" id="step-1" style="display: block;">
<h2>1. Project Basics</h2>
<div class="notice">Define the core project information that will be inherited by all Work Packages.</div>
<div class="field-grid">
<div class="field">
<label>Project Name *</label>
<input type="text" id="proj_name" placeholder="e.g., Micron — INC Construction Work Packages">
</div>
<div class="field">
<label>Project Number *</label>
<input type="text" id="proj_number" placeholder="e.g., 26-67-008">
</div>
<div class="field">
<label>Client Name *</label>
<input type="text" id="proj_client" placeholder="e.g., Micron Technology, Inc.">
</div>
<div class="field">
<label>Division / Sector *</label>
<input type="text" id="proj_division" placeholder="e.g., Semiconductor, Oil & Gas, Data Center">
</div>
<div class="field">
<label>Site Location *</label>
<input type="text" id="proj_site" placeholder="e.g., Boise, ID — Fab 7">
</div>
</div>
</div>
<!-- STEP 2: PROJECT TEAM -->
<div class="step" id="step-2" style="display: none;">
<h2>2. Project Team Leadership</h2>
<div class="notice">Name the key project leaders. These are informational and will appear in SOP exports.</div>
<div class="field-grid">
<div class="field">
<label>Project Manager (PM)</label>
<input type="text" id="proj_pm" placeholder="e.g., Nick Siegfried">
</div>
<div class="field">
<label>Construction Manager (CM)</label>
<input type="text" id="proj_cm" placeholder="e.g., K. Boyd">
</div>
<div class="field">
<label>Quality Manager (QM)</label>
<input type="text" id="proj_qm" placeholder="e.g., D. Nguyen">
</div>
</div>
</div>
<!-- STEP 3: SIGN-OFF ROLES -->
<div class="step" id="step-3" style="display: none;">
<h2>3. Required Sign-Off Roles</h2>
<div class="notice">Superintendent and Foreman are required. Add other roles as needed for your project structure.</div>
<div class="required-roles">
<div class="role-required">
<div class="role-checkbox">
<input type="checkbox" id="role_super" checked disabled>
<label>Superintendent *</label>
</div>
<input type="text" id="role_super_name" placeholder="Name (optional)" style="flex: 1; margin-left: 1rem;">
</div>
<div class="role-required">
<div class="role-checkbox">
<input type="checkbox" id="role_foreman" checked disabled>
<label>Foreman *</label>
</div>
<input type="text" id="role_foreman_name" placeholder="Name (optional)" style="flex: 1; margin-left: 1rem;">
</div>
</div>
<div style="margin-top: 2rem; border-top: 1px solid var(--border); padding-top: 1.5rem;">
<div class="sub-heading">Optional Additional Roles</div>
<div id="optional-roles-list" style="margin-top: 1rem;">
<!-- Added dynamically -->
</div>
<button class="add-btn" onclick="addOptionalRole()">+ Add Role</button>
</div>
</div>
<!-- STEP 4: WORK PACKAGE TYPES -->
<div class="step" id="step-4" style="display: none;">
<h2>4. Work Package Types & Disciplines</h2>
<div class="notice">Enable the WP types your project will use. Assign a discipline to each for IWP constraint checklists.</div>
<div id="wp-types-table" style="margin-top: 1.5rem;">
<!-- Built dynamically -->
</div>
</div>
<!-- STEP 5: GOVERNANCE -->
<div class="step" id="step-5" style="display: none;">
<h2>5. Governance & WP Numbering</h2>
<div class="notice">Define how Work Packages are formatted, sized, and issued on this project.</div>
<div class="field-grid">
<div class="field">
<label>Work Package Number Format *</label>
<input type="text" id="gov_woformat" placeholder="e.g., WP##-[Sector]-[Discipline]-[TYPE]">
<small>Use ## for counter, [Sector] [Discipline] [TYPE] as variables</small>
</div>
<div class="field">
<label>Typical WP Size</label>
<input type="text" id="gov_wosize" placeholder="e.g., 35 days or 4080 hours">
</div>
<div class="field">
<label>Issuance Strategy</label>
<select id="gov_issuance" multiple size="3">
<option selected>By Sector / Area</option>
<option>By Discipline</option>
<option>By Phase / Sequence</option>
<option>By Resource Availability</option>
</select>
<small>Hold Ctrl to select multiple</small>
</div>
</div>
</div>
<!-- STEP 6: QUALITY -->
<div class="step" id="step-6" style="display: none;">
<h2>6. Quality & Inspection Strategy</h2>
<div class="notice">Establish project-wide quality expectations that cascade to every Work Package.</div>
<div class="field-grid col1">
<div class="field">
<label>QC Required? *</label>
<select id="qual_qcreq">
<option>Yes</option>
<option>Yes — Detailed inspection items</option>
<option>Yes — Sample / Spot-check</option>
<option>No</option>
</select>
</div>
<div class="field">
<label>Photo / Documentation Standard</label>
<select id="qual_photo">
<option selected>Key checkpoints only</option>
<option>Every step documented</option>
<option>As-built final condition only</option>
<option>None</option>
</select>
</div>
<div class="field">
<label>Hold Points & Witness Requirements</label>
<textarea id="qual_hold" rows="3" placeholder="e.g., HOLD: Prime QAQC to inspect rough-in before cover/cover-up. WITNESS: client QC to observe megger test before energization."></textarea>
</div>
</div>
</div>
<!-- STEP 7: PLATFORMS -->
<div class="step" id="step-7" style="display: none;">
<h2>7. Tracking & Commissioning Platforms</h2>
<div class="notice">Select the tools used for construction tracking and commissioning. These can be the same or different systems.</div>
<div class="field-grid">
<div class="field">
<label>Construction Tracking Platform *</label>
<select id="plat_tracking">
<option>CxAlloy</option>
<option>Procore</option>
<option>Autodesk Construction Cloud (ACC)</option>
<option>Other</option>
</select>
<small>Primary tool for WP status, progress, and reporting</small>
</div>
<div class="field">
<label>Commissioning Tool *</label>
<select id="plat_commissioning">
<option selected>CxAlloy</option>
<option>Procore</option>
<option>Autodesk Construction Cloud (ACC)</option>
<option>Other</option>
</select>
<small>Tool for system turnover, testing, and sign-offs</small>
</div>
</div>
</div>
<!-- STEP 8: SEQUENCE -->
<div class="step" id="step-8" style="display: none;">
<h2>8. Construction Sequence</h2>
<div class="notice">Define the major phases and logical sequence of construction work. These become predecessors in WP planning.</div>
<div id="sequence-list" style="margin-top: 1rem;">
<!-- Built dynamically -->
</div>
<button class="add-btn" onclick="addSequenceStep()">+ Add Sequence Step</button>
</div>
<!-- STEP 9: CONSTRAINTS -->
<div class="step" id="step-9" style="display: none;">
<h2>9. Release Gate Constraints</h2>
<div class="notice">Constraints are readiness items that must be cleared before a WP can be issued. Start with the standard 10 (AWP Vol II), or add custom constraints per your project needs.</div>
<div style="margin-bottom: 2rem;">
<div class="sub-heading">Standard AWP Constraints (Vol II §2.3.2)</div>
<div id="standard-constraints" style="margin-top: 1rem;">
<!-- Built dynamically -->
</div>
</div>
<div style="border-top: 1px solid var(--border); padding-top: 1.5rem;">
<div class="sub-heading">Custom Constraints (Optional)</div>
<div id="custom-constraints-list" style="margin-top: 1rem;">
<!-- Built dynamically -->
</div>
<button class="add-btn" onclick="showConstraintLibrary()">+ Add Custom Constraint</button>
</div>
</div>
<!-- STEP 10: SOURCES & REFERENCES -->
<div class="step" id="step-10" style="display: none;">
<h2>10. Engineering Sources & References</h2>
<div class="notice">Link to key documents and systems that WP authors will reference (Procore drawings, IO lists, cable schedules, specifications, etc.).</div>
<div id="sources-list" style="margin-top: 1rem;">
<!-- Built dynamically -->
</div>
<button class="add-btn" onclick="addSource()">+ Add Source</button>
</div>
</div>
<!-- COMMENTS SECTION -->
<div class="comments-section">
<button class="comments-toggle" onclick="toggleComments()">💬 Step Comments</button>
<div id="comments-panel" style="display: none; margin-top: 1rem; padding: 1rem; background: var(--bg); border-radius: 6px; border: 1px solid var(--border);">
<div style="margin-bottom: 1rem;">
<label style="font-weight: 600; font-size: 13px;">Your Name (optional)</label>
<input type="text" id="commenter-name" placeholder="e.g., Bill Clarida" style="width: 100%; padding: 0.5rem; border: 1px solid var(--border); border-radius: 4px; margin-top: 0.25rem;">
</div>
<div style="margin-bottom: 1rem;">
<label style="font-weight: 600; font-size: 13px;">Feedback on Step {{STEP}}</label>
<textarea id="comment-text" rows="3" placeholder="e.g., 'This field should be higher in the form' or 'Good, but missing [X] option'" style="width: 100%; padding: 0.5rem; border: 1px solid var(--border); border-radius: 4px; margin-top: 0.25rem; font-family: inherit;"></textarea>
</div>
<button onclick="submitComment()" style="background: var(--primary); color: white; padding: 0.5rem 1rem; border: none; border-radius: 4px; cursor: pointer; font-weight: 600;">Submit Comment</button>
<button onclick="toggleComments()" style="background: var(--border); color: var(--text); padding: 0.5rem 1rem; border: none; border-radius: 4px; cursor: pointer; margin-left: 0.5rem;">Close</button>
<div id="comments-list" style="margin-top: 1rem; max-height: 200px; overflow-y: auto;">
<!-- Comments loaded here -->
</div>
</div>
</div>
<!-- NAVIGATION -->
<div class="step-navigation">
<button class="nav-btn" id="prev-btn" onclick="previousStep()">← Back</button>
<button class="nav-btn" id="next-btn" onclick="nextStep()">Next →</button>
<button class="nav-btn primary" id="export-btn" onclick="exportSOP()" style="display: none;">⤓ Export SOP JSON</button>
</div>
</div>
<!-- CONSTRAINT LIBRARY MODAL -->
<div id="constraint-modal" class="modal" style="display: none;">
<div class="modal-content">
<div class="modal-header">
<h3>Add Custom Constraint</h3>
<button class="modal-close" onclick="closeConstraintModal()"></button>
</div>
<div id="constraint-library" style="max-height: 400px; overflow-y: auto; margin: 1rem 0;">
<!-- Built dynamically -->
</div>
<button class="nav-btn" onclick="closeConstraintModal()">Done</button>
</div>
</div>
<script src="sop-config-app.js"></script>
</body>
</html>

View File

@@ -6,7 +6,8 @@ let allComments = [];
let state = {
project: {name:'', number:'', client:'', division:'', site:''},
team: {pm:'', cm:'', qm:''},
team: {pm:'', apm:'', cm:'', qm:''},
teamMembers: [],
signoffRoles: [{role:'Superintendent',name:''},{role:'Foreman',name:''}],
wpTypes: [],
governance: {woformat:'', wosize:'', issuance:[]},
@@ -94,6 +95,7 @@ const CONSTRAINT_LIBRARY = [
];
const OPTIONAL_ROLES = [
'Assistant Project Manager',
'General Foreman',
'Construction Manager',
'HSE Professional',
@@ -148,13 +150,31 @@ const LABOR_COST_CODES = [
// ── INITIALIZATION ────────────────────────────────────────────────────────────
window.addEventListener('DOMContentLoaded',()=>{
initializeWPTypes();
renderTeamMembers();
renderOptionalRoles();
renderStandardConstraints();
renderSequenceSteps();
renderSources();
restoreSavedSOP();
updateStepUI();
updateProjectDisplay();
// Deep-link: ?tab=sop | ?tab=wp from the home page cards.
const params = new URLSearchParams(window.location.search);
const tab = params.get('tab');
if(tab === 'wp' || tab === 'sop') switchTool(tab);
track('app_open');
let _fieldTimer;
document.addEventListener('input', e=>{
const t = e.target;
if(t && t.id && /^(INPUT|SELECT|TEXTAREA)$/.test(t.tagName)){
clearTimeout(_fieldTimer);
_fieldTimer = setTimeout(()=> track('field_edit', {field:t.id}), 600);
}
});
});
window.addEventListener('beforeunload', trackStepDwell);
function initializeWPTypes(){
state.wpTypes = JSON.parse(JSON.stringify(DEFAULT_WP_TYPES)).map(t=>({...t,discipline:TYPE_DISCIPLINE_MAP[t.name]||''}));
@@ -164,14 +184,15 @@ function initializeWPTypes(){
// ── LOAD SAMPLE DATA ──────────────────────────────────────────────────────────
function loadSampleData(){
// Populate Step 1
document.getElementById('proj_name').value = 'Micron — INC Construction Work Packages';
document.getElementById('proj_name').value = 'MICRON_PH1_CUP_HPM_FMCS INSTALL';
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_pm').value = 'Mariano Sanchez';
document.getElementById('proj_apm').value = 'Assistant PM';
document.getElementById('proj_cm').value = 'K. Boyd';
document.getElementById('proj_qm').value = 'D. Nguyen';
@@ -192,7 +213,8 @@ function loadSampleData(){
// Collect all data
collectStepData();
track('sample_loaded');
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
@@ -201,6 +223,55 @@ function loadSampleData(){
updateProjectDisplay();
}
// ── RESTORE A COMPLETED SOP (for "Review" + WP tab across reloads) ─────────────
function restoreSavedSOP(){
let savedState = null, savedSop = null, complete = false;
try {
complete = localStorage.getItem('wp_suite_sop_complete') === '1';
savedState = JSON.parse(localStorage.getItem('wp_suite_state') || 'null');
savedSop = JSON.parse(localStorage.getItem('wp_suite_sop') || 'null');
} catch(e){}
if(!complete || !savedState) return;
state = savedState;
sop = savedSop;
sopComplete = true;
// Re-render dynamic lists from restored state.
renderWPTypes();
renderTeamMembers();
renderOptionalRoles();
renderStandardConstraints();
renderSequenceSteps();
renderSources();
repopulateForm();
if(typeof onSOPReady === 'function') onSOPReady(sop);
}
// Push restored state values back into the static form inputs.
function repopulateForm(){
const set = (id,val)=>{ const el=document.getElementById(id); if(el!=null && val!=null) el.value = val; };
set('proj_name', state.project.name);
set('proj_number', state.project.number);
set('proj_client', state.project.client);
set('proj_division', state.project.division);
set('proj_site', state.project.site);
set('proj_pm', state.team.pm);
set('proj_apm', state.team.apm);
set('proj_cm', state.team.cm);
set('proj_qm', state.team.qm);
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);
set('gov_wosize', state.governance.wosize);
set('qual_qcreq', state.quality.qcreq);
set('qual_photo', state.quality.photo);
set('qual_hold', state.quality.hold);
set('plat_tracking', state.platforms.tracking);
set('plat_commissioning', state.platforms.commissioning);
}
// ── TOOL SWITCHING ────────────────────────────────────────────────────────────
function switchTool(tool){
currentTool = tool;
@@ -219,11 +290,34 @@ function switchTool(tool){
}else{
document.getElementById('total-steps').textContent = '—';
}
if(tool === 'wp') renderWPTab();
updateStepUI();
updateProjectDisplay();
}
// Show the gate or the embedded Work Package Creator depending on SOP status.
function renderWPTab(){
const gate = document.getElementById('wp-gate');
const frame = document.getElementById('wp-frame');
if(!gate || !frame) return;
if(sopComplete){
gate.style.display = 'none';
frame.style.display = 'block';
// Reload each time so the creator picks up the latest SOP from localStorage.
frame.src = 'wp-creation-index.html?embedded=1&t=' + Date.now();
}else{
gate.style.display = 'block';
frame.style.display = 'none';
}
}
// Called from completeSOP / restoreSavedSOP once an SOP is available.
function onSOPReady(){
if(currentTool === 'wp') renderWPTab();
}
function updateProjectDisplay(){
const projName = document.getElementById('proj_name')?.value || 'Project';
const display = document.getElementById('project-display');
@@ -255,6 +349,28 @@ function toggleWPType(i){
renderWPTypes();
}
function renderTeamMembers(){
const container = document.getElementById('team-members-list');
if(!container) return;
container.innerHTML = state.teamMembers.map((m,i)=>`
<div style="display:grid; grid-template-columns:1fr 1fr 30px; gap:1rem; align-items:center; padding:0.75rem; background:var(--bg); border-radius:6px; margin-bottom:0.5rem; border:1px solid var(--border);">
<input type="text" placeholder="Role / title (e.g., Scheduler)" value="${(m.role||'').replace(/"/g,'&quot;')}" onchange="state.teamMembers[${i}].role=this.value" style="padding:0.5rem; border:1px solid var(--border); border-radius:4px;">
<input type="text" placeholder="Name" value="${(m.name||'').replace(/"/g,'&quot;')}" onchange="state.teamMembers[${i}].name=this.value" style="padding:0.5rem; border:1px solid var(--border); border-radius:4px;">
<button style="background:var(--danger); color:white; padding:0.25rem; width:30px; height:30px; border:none; border-radius:4px; cursor:pointer; font-weight:600;" onclick="removeTeamMember(${i})">✕</button>
</div>
`).join('');
}
function addTeamMember(){
state.teamMembers.push({role:'',name:''});
renderTeamMembers();
}
function removeTeamMember(i){
state.teamMembers.splice(i,1);
renderTeamMembers();
}
function renderOptionalRoles(){
const container = document.getElementById('optional-roles-list');
const current = state.signoffRoles.filter(r=>r.role!=='Superintendent'&&r.role!=='Foreman');
@@ -323,38 +439,86 @@ function addCustomConstraint(name){
renderStandardConstraints();
}
const DEFAULT_SEQUENCE = ['Layout','Conduit Install','Tray Install','Wire Pull','Device Install','Termination','QC Inspection','Commissioning'];
let seqDragIndex = null;
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 style="display:grid; grid-template-columns:200px 100px 30px; gap:1rem; align-items:center; padding:1rem; background:var(--bg); border-radius:6px; margin-bottom:0.5rem; border:1px solid var(--border);">
<input type="text" value="${s.label}" placeholder="Step name" onchange="state.sequence[${i}].label=this.value" style="padding:0.5rem; border:1px solid var(--border); border-radius:4px;">
<select onchange="state.sequence[${i}].kind=this.value" style="padding:0.5rem; border:1px solid var(--border); border-radius:4px;">
<option ${s.kind==='step'?'selected':''}>step</option>
<option ${s.kind==='gate'?'selected':''}>gate</option>
</select>
<button style="background:var(--danger); color:white; padding:0.25rem; width:30px; height:30px; border:none; border-radius:4px; cursor:pointer; font-weight:600;" onclick="state.sequence.splice(${i},1); renderSequenceSteps()">✕</button>
</div>
`).join('');
if(!container) return;
if(!state.sequence.length) state.sequence = DEFAULT_SEQUENCE.map(s=>({label:s,kind:'step'}));
container.innerHTML = '';
let stepNo = 0;
state.sequence.forEach((item,i)=>{
const isGate = item.kind==='gate';
if(!isGate) stepNo++;
const row = document.createElement('div');
row.className = 'seq-step' + (isGate?' gate':'');
row.draggable = true;
row.dataset.idx = i;
const badge = isGate ? `<span class="seq-gate-badge" title="QC / hold gate">◆ HOLD</span>`
: `<span class="seq-num">${stepNo}</span>`;
row.innerHTML = `
<span class="seq-handle" title="Drag to reorder">⠿</span>
${badge}
<input type="text" class="seq-label" value="${(item.label||'').replace(/"/g,'&quot;')}" oninput="state.sequence[${i}].label=this.value">
<button class="seq-del" title="Remove" onclick="removeSeqStep(${i})">✕</button>`;
row.addEventListener('dragstart',e=>{ seqDragIndex=i; row.classList.add('dragging'); e.dataTransfer.effectAllowed='move'; });
row.addEventListener('dragend',()=>{ row.classList.remove('dragging'); document.querySelectorAll('.seq-step').forEach(s=>s.classList.remove('drag-over')); });
row.addEventListener('dragover',e=>{ e.preventDefault(); row.classList.add('drag-over'); e.dataTransfer.dropEffect='move'; });
row.addEventListener('dragleave',()=>row.classList.remove('drag-over'));
row.addEventListener('drop',e=>{
e.preventDefault(); row.classList.remove('drag-over');
const from=seqDragIndex, to=i;
if(from===null||from===to) return;
const moved=state.sequence.splice(from,1)[0];
state.sequence.splice(to,0,moved);
seqDragIndex=null; renderSequenceSteps();
});
container.appendChild(row);
if(i<state.sequence.length-1){ const a=document.createElement('div'); a.className='seq-arrow'; a.textContent='↓'; container.appendChild(a); }
});
}
function addSequenceStep(){
state.sequence.push({label:'',kind:'step'});
const inp = document.getElementById('seq-add-input');
const v = (inp && inp.value || '').trim();
state.sequence.push({label: v || 'New Step', kind:'step'});
if(inp) inp.value = '';
renderSequenceSteps();
}
function addSequenceGate(){
state.sequence.push({label:'QC Hold', kind:'gate'});
renderSequenceSteps();
}
function removeSeqStep(i){
state.sequence.splice(i,1);
renderSequenceSteps();
}
const DEFAULT_SOURCES = [
{label:'Design Drawings', ph:'e.g. Procore, Bluebeam'},
{label:'Spool Drawings', ph:'e.g. SharePoint, BIM'},
{label:'Installation Detail Drawings', ph:'e.g. Prime details + Micron MVDs'},
{label:'IO List', ph:'e.g. controls.dev, Excel'},
{label:'Cable Schedule', ph:'e.g. Excel on SharePoint'},
{label:'Conduit Schedule', ph:'e.g. Excel on SharePoint'},
{label:'Tray Schedule', ph:'e.g. Excel on SharePoint'},
{label:'Datasheets', ph:'e.g. Procore'},
{label:'Specifications', ph:'e.g. client portal'},
{label:'Asset Register', ph:'e.g. CxAlloy, Excel'},
{label:'RFIs', ph:'e.g. Procore RFI log'},
{label:'Safety Documentation', ph:'e.g. site safety binder'}
];
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:''}
];
if(!state.sources.length) state.sources = DEFAULT_SOURCES.map(s=>({label:s.label, system:'', notes:'', link:'', ph:s.ph}));
container.innerHTML = state.sources.map((s,i)=>`
<div style="display:grid; grid-template-columns:150px 150px 250px 150px 30px; gap:1rem; align-items:center; padding:1rem; background:var(--bg); border-radius:6px; margin-bottom:0.5rem; border:1px solid var(--border);">
<input type="text" value="${s.label}" placeholder="Label" onchange="state.sources[${i}].label=this.value" style="padding:0.5rem; font-size:12px; border:1px solid var(--border); border-radius:4px;">
<input type="text" value="${s.system}" placeholder="System" onchange="state.sources[${i}].system=this.value" style="padding:0.5rem; font-size:12px; border:1px solid var(--border); border-radius:4px;">
<input type="text" value="${s.system}" placeholder="${s.ph||'System of record'}" onchange="state.sources[${i}].system=this.value" style="padding:0.5rem; font-size:12px; border:1px solid var(--border); border-radius:4px;">
<input type="text" value="${s.link}" placeholder="URL" onchange="state.sources[${i}].link=this.value" style="padding:0.5rem; font-size:12px; border:1px solid var(--border); border-radius:4px;">
<input type="text" value="${s.notes}" placeholder="Notes" onchange="state.sources[${i}].notes=this.value" style="padding:0.5rem; font-size:12px; border:1px solid var(--border); border-radius:4px;">
<button style="background:var(--danger); color:white; padding:0.25rem; width:30px; height:30px; border:none; border-radius:4px; cursor:pointer; font-weight:600;" onclick="state.sources.splice(${i},1); renderSources()">✕</button>
@@ -390,6 +554,8 @@ function previousStep(){
}
function updateStepUI(){
trackStepDwell();
track('step_view', {step: currentStep});
// SOP steps
document.querySelectorAll('[id^="sop-step-"]').forEach(s=>s.style.display='none');
document.getElementById(`sop-step-${currentStep}`)?.style.display && (document.getElementById(`sop-step-${currentStep}`).style.display='block');
@@ -421,6 +587,7 @@ function collectStepData(){
break;
case 2:
state.team.pm = document.getElementById('proj_pm').value;
state.team.apm = document.getElementById('proj_apm').value;
state.team.cm = document.getElementById('proj_cm').value;
state.team.qm = document.getElementById('proj_qm').value;
break;
@@ -478,9 +645,11 @@ function completeSOP(){
client: state.project.client,
division: state.project.division,
pm: state.team.pm,
apm: state.team.apm,
cm: state.team.cm,
qm: state.team.qm,
site: state.project.site
site: state.project.site,
teamMembers: state.teamMembers.filter(m=>(m.role||m.name))
},
roles: state.signoffRoles.filter(r=>r.role),
governance: {
@@ -512,12 +681,20 @@ function completeSOP(){
sopComplete = true;
updateProjectDisplay();
// Persist for the home page (green / "Review") and for the WP Creator tab.
try {
localStorage.setItem('wp_suite_sop', JSON.stringify(sop));
localStorage.setItem('wp_suite_state', JSON.stringify(state));
localStorage.setItem('wp_suite_sop_complete', '1');
} catch(e){}
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.');
// Update WP tool status
document.getElementById('sop-status').textContent = '✓ Ready';
document.getElementById('sop-progress-text').textContent = `Project: ${sop.project.name} | ${sop.woTypes.length} WP Types | ${sop.constraints.length} Constraints`;
// Hand the SOP to the embedded Work Package Creator and unlock its tab.
if(typeof onSOPReady === 'function') onSOPReady(sop);
}
// ── COMMENTS ──────────────────────────────────────────────────────────────────
@@ -552,10 +729,10 @@ function submitComment(){
function loadStepComments(){
const saved = localStorage.getItem('wp_suite_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{
@@ -567,3 +744,65 @@ function loadStepComments(){
`).join('');
}
}
// ── USAGE ANALYTICS ─────────────────────────────────────────────────────────
// Lightweight usage analytics stored in localStorage so the tool owner can review
// engagement over time. No field VALUES are stored (field-edit events record only
// the field id), keeping captured data non-sensitive.
const ANALYTICS_KEY = 'wp_suite_analytics_v1';
let _stepEnter = Date.now();
const _session = 's_' + Date.now().toString(36) + Math.random().toString(36).slice(2,6);
function analyticsLoad(){
try { return JSON.parse(localStorage.getItem(ANALYTICS_KEY)) || {events:[]}; }
catch(e){ return {events:[]}; }
}
function analyticsSave(data){
try { localStorage.setItem(ANALYTICS_KEY, JSON.stringify(data)); }
catch(e){ /* storage unavailable — degrade silently */ }
}
function track(event, detail){
try {
const data = analyticsLoad();
data.events.push({ ts: new Date().toISOString(), session: _session, event, detail: detail||null });
if(data.events.length > 5000) data.events = data.events.slice(-5000);
analyticsSave(data);
} catch(e){}
}
function trackStepDwell(){
const ms = Date.now() - _stepEnter;
if(ms > 400 && ms < 1000*60*60) track('step_dwell', {step: currentStep, ms});
_stepEnter = Date.now();
}
function analyticsSummary(){
const data = analyticsLoad();
const byEvent = {}, byStep = {}, dwell = {}, sessions = new Set();
data.events.forEach(e=>{
byEvent[e.event] = (byEvent[e.event]||0)+1;
sessions.add(e.session);
if(e.event==='step_view' && e.detail) byStep[e.detail.step]=(byStep[e.detail.step]||0)+1;
if(e.event==='step_dwell' && e.detail){ dwell[e.detail.step]=(dwell[e.detail.step]||0)+e.detail.ms; }
});
return {total:data.events.length, sessions:sessions.size, byEvent, byStep, dwell, first:data.events[0]?.ts, last:data.events[data.events.length-1]?.ts};
}
function showAnalytics(){
const s = analyticsSummary();
const fmtMin = ms => (ms/60000).toFixed(1)+' min';
let txt = `USAGE LOGS\n\nSessions: ${s.sessions} Events: ${s.total}\nRange: ${s.first?new Date(s.first).toLocaleString():'—'}${s.last?new Date(s.last).toLocaleString():'—'}\n\nStep views:\n`;
for(let i=1;i<=10;i++) txt += ` Step ${i}: ${s.byStep[i]||0} views` + (s.dwell[i]?`, ${fmtMin(s.dwell[i])} total`:'') + `\n`;
txt += `\nActions:\n`;
Object.keys(s.byEvent).filter(k=>!['step_view','step_dwell','field_edit'].includes(k)).forEach(k=> txt += ` ${k}: ${s.byEvent[k]}\n`);
txt += ` field edits: ${s.byEvent['field_edit']||0}\n`;
txt += `\nDownload full event log as JSON?`;
if(confirm(txt)) downloadAnalytics();
}
function downloadAnalytics(){
const data = analyticsLoad();
const blob = new Blob([JSON.stringify(data,null,2)], {type:'application/json'});
const a = document.createElement('a');
a.href = URL.createObjectURL(blob);
a.download = 'wp-suite-usage-' + new Date().toISOString().slice(0,10) + '.json';
a.click();
URL.revokeObjectURL(a.href);
track('analytics_exported');
}

View File

@@ -30,17 +30,18 @@ body {
/* HEADER */
.header {
background: linear-gradient(135deg, var(--primary) 0%, #1e40af 100%);
color: white;
background: #ffffff;
color: var(--text);
padding: 1.5rem 2rem;
display: flex;
justify-content: space-between;
align-items: center;
box-shadow: var(--shadow-lg);
box-shadow: var(--shadow);
border-bottom: 1px solid var(--border);
}
.header-left {
flex: 1;
.header-left {
flex: 1;
display: flex;
align-items: center;
gap: 1.5rem;
@@ -51,18 +52,18 @@ body {
align-items: center;
gap: 0.5rem;
text-decoration: none;
color: white;
color: var(--text);
font-weight: 700;
font-size: 14px;
transition: opacity 0.2s;
}
.logo:hover { opacity: 0.9; }
.logo:hover { opacity: 0.7; }
.logo-icon {
width: 36px;
height: 36px;
background: rgba(255,255,255,0.2);
background: var(--primary-light);
border-radius: 6px;
display: flex;
align-items: center;
@@ -71,22 +72,16 @@ body {
font-weight: 700;
}
.logo-text {
display: flex;
flex-direction: column;
line-height: 1;
font-size: 12px;
}
.header-title {
font-size: 24px;
font-weight: 700;
margin-bottom: 0;
color: var(--text);
}
.header-subtitle {
font-size: 13px;
opacity: 0.9;
color: var(--text-light);
min-height: 20px;
}
@@ -98,9 +93,9 @@ body {
.header-button {
padding: 0.5rem 1rem;
background: rgba(255,255,255,0.2);
color: white;
border: 1px solid rgba(255,255,255,0.4);
background: var(--bg);
color: var(--primary);
border: 1px solid var(--border);
border-radius: 6px;
cursor: pointer;
font-size: 13px;
@@ -109,12 +104,14 @@ body {
}
.header-button:hover {
background: rgba(255,255,255,0.3);
border-color: white;
background: var(--primary-light);
border-color: var(--primary);
}
.step-counter {
background: rgba(255,255,255,0.2);
background: var(--bg);
border: 1px solid var(--border);
color: var(--text-light);
padding: 0.4rem 0.8rem;
border-radius: 20px;
font-size: 12px;
@@ -397,6 +394,71 @@ body {
.comments-toggle:hover { background: var(--primary); color: white; }
/* CONSTRUCTION SEQUENCE (drag & drop) */
#sequence-list { display: flex; flex-direction: column; gap: 8px; }
.seq-step {
display: flex; align-items: center; gap: 12px; padding: 11px 14px;
background: var(--bg-card); border: 1px solid var(--border); border-radius: 6px;
box-shadow: var(--shadow); transition: border-color .12s, box-shadow .12s, opacity .12s;
}
.seq-step:hover { border-color: var(--primary); }
.seq-step.dragging { opacity: .35; border-style: dashed; }
.seq-step.drag-over { border-color: var(--primary); box-shadow: 0 0 0 3px var(--primary-light); }
.seq-handle { cursor: grab; color: var(--text-dim); font-size: 16px; line-height: 1; user-select: none; flex-shrink: 0; }
.seq-handle:active { cursor: grabbing; }
.seq-num {
width: 24px; height: 24px; flex-shrink: 0; border-radius: 50%; background: var(--primary-light); color: var(--primary);
font-size: 11px; font-weight: 600; display: flex; align-items: center; justify-content: center;
}
.seq-step input.seq-label {
border: 1px solid transparent; background: transparent; font-size: 14px; padding: 5px 8px; color: var(--text); flex: 1; border-radius: 4px;
}
.seq-step input.seq-label:focus { background: var(--bg); border-color: var(--primary); outline: none; }
.seq-del {
background: var(--danger); color: white; border: none; border-radius: 4px;
width: 28px; height: 28px; cursor: pointer; font-weight: 600; flex-shrink: 0;
}
.seq-arrow { text-align: center; color: var(--text-dim); font-size: 13px; line-height: .4; margin: -2px 0; }
.seq-step.gate { border-color: var(--warning); background: #fff7ed; border-style: dashed; }
.seq-step.gate .seq-label { color: var(--warning); font-weight: 500; }
.seq-gate-badge {
flex-shrink: 0; padding: 3px 9px; border-radius: 20px; background: var(--warning); color: #fff;
font-size: 9px; font-weight: 600; letter-spacing: .08em; white-space: nowrap;
}
/* STEP COMMENTS DROPDOWN */
.comments-dropdown {
position: fixed;
top: 80px;
right: 2rem;
width: 360px;
max-width: calc(100vw - 2rem);
background: var(--bg-card);
border: 1px solid var(--border);
border-radius: 8px;
box-shadow: var(--shadow-lg);
padding: 1.25rem;
z-index: 1200;
}
.comments-dropdown-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1rem;
}
.comments-dropdown-close {
background: none;
border: none;
font-size: 16px;
cursor: pointer;
color: var(--text-light);
line-height: 1;
}
.comments-dropdown-close:hover { color: var(--text); }
/* MODAL */
.modal {
position: fixed;

View File

@@ -14,13 +14,16 @@
<div class="header-left">
<a href="index.html" class="logo" title="Back to Home">
<img src="prime-controls-logo.jpg" alt="Prime Controls" style="height: 36px; width: auto;">
<div class="logo-text">Work Package Suite</div>
</a>
<div class="header-title">Work Package Suite</div>
<div class="header-subtitle" id="project-display"></div>
<div>
<div class="header-title">Work Package Suite</div>
<div class="header-subtitle" id="project-display"></div>
</div>
</div>
<div class="header-right">
<button id="load-sample-btn" class="header-button" onclick="loadSampleData()" title="Load example SOP data">⭐ Load Sample</button>
<button class="header-button" onclick="toggleComments()" title="View and add comments for the current step">💬 Step Comments</button>
<button class="header-button" onclick="showAnalytics()" title="Review usage logs for this tool">📊 Usage Logs</button>
<span class="step-counter"><span id="current-step">1</span> / <span id="total-steps">10</span></span>
</div>
</div>
@@ -97,7 +100,11 @@
<div class="field-grid">
<div class="field">
<label>Project Manager (PM)</label>
<input type="text" id="proj_pm" placeholder="e.g., Nick Siegfried">
<input type="text" id="proj_pm" placeholder="e.g., Mariano Sanchez">
</div>
<div class="field">
<label>Assistant Project Manager (APM)</label>
<input type="text" id="proj_apm" placeholder="e.g., Assistant PM name">
</div>
<div class="field">
<label>Construction Manager (CM)</label>
@@ -108,6 +115,11 @@
<input type="text" id="proj_qm" placeholder="e.g., D. Nguyen">
</div>
</div>
<div style="margin-top: 2rem; border-top: 1px solid var(--border); padding-top: 1.5rem;">
<div class="sub-heading">Additional Team Members (optional)</div>
<div id="team-members-list" style="margin-top: 1rem;"></div>
<button class="add-btn" onclick="addTeamMember()">+ Add Team Member</button>
</div>
</div>
<!-- STEP 3: SIGN-OFF ROLES -->
@@ -230,9 +242,13 @@
<!-- STEP 8: SEQUENCE -->
<div class="step" id="sop-step-8" style="display: none;">
<h2>8. Construction Sequence</h2>
<div class="notice">Define the major phases and logical sequence of construction work.</div>
<div class="notice">Comes pre-sequenced with a typical install flow. Drag the ⠿ handle to reorder steps, edit labels inline, add a QC hold gate, or remove any step.</div>
<div id="sequence-list" style="margin-top: 1rem;"></div>
<button class="add-btn" onclick="addSequenceStep()">+ Add Sequence Step</button>
<div style="display:flex; gap:0.5rem; margin-top:1rem; flex-wrap:wrap;">
<input type="text" id="seq-add-input" placeholder="New step name" onkeydown="if(event.key==='Enter'){addSequenceStep();}" style="flex:1; min-width:200px; padding:0.5rem; border:1px solid var(--border); border-radius:4px;">
<button class="add-btn" onclick="addSequenceStep()">+ Add Step</button>
<button class="add-btn" onclick="addSequenceGate()" style="background:var(--warning);">◆ Add QC Hold</button>
</div>
</div>
<!-- STEP 9: CONSTRAINTS -->
@@ -260,24 +276,6 @@
</div>
<!-- SOP COMMENTS -->
<div class="comments-section">
<button class="comments-toggle" onclick="toggleComments()">💬 Step Comments</button>
<div id="comments-panel" style="display: none; margin-top: 1rem; padding: 1rem; background: var(--bg); border-radius: 6px; border: 1px solid var(--border);">
<div style="margin-bottom: 1rem;">
<label style="font-weight: 600; font-size: 13px;">Your Name (optional)</label>
<input type="text" id="commenter-name" placeholder="e.g., Bill Clarida" style="width: 100%; padding: 0.5rem; border: 1px solid var(--border); border-radius: 4px; margin-top: 0.25rem;">
</div>
<div style="margin-bottom: 1rem;">
<label style="font-weight: 600; font-size: 13px;">Feedback</label>
<textarea id="comment-text" rows="3" placeholder="Your feedback here..." style="width: 100%; padding: 0.5rem; border: 1px solid var(--border); border-radius: 4px; margin-top: 0.25rem; font-family: inherit;"></textarea>
</div>
<button onclick="submitComment()" style="background: var(--primary); color: white; padding: 0.5rem 1rem; border: none; border-radius: 4px; cursor: pointer; font-weight: 600;">Submit</button>
<button onclick="toggleComments()" style="background: var(--border); color: var(--text); padding: 0.5rem 1rem; border: none; border-radius: 4px; cursor: pointer; margin-left: 0.5rem;">Close</button>
<div id="comments-list" style="margin-top: 1rem; max-height: 200px; overflow-y: auto;"></div>
</div>
</div>
<!-- SOP NAVIGATION -->
<div class="step-navigation">
<button class="nav-btn" id="sop-prev-btn" onclick="previousStep()">← Back</button>
@@ -291,20 +289,38 @@
<!-- WORK PACKAGE CREATION TOOL -->
<!-- ═════════════════════════════════════════════════════════════════════ -->
<div id="tool-wp" class="tool">
<div style="padding: 2rem; text-align: center;">
<!-- Shown until the SOP is complete -->
<div id="wp-gate" style="padding: 3rem 2rem; text-align: center;">
<h2>📋 Work Package Creation</h2>
<p style="color: var(--text-light); margin: 1rem 0;">Complete the SOP Configuration first to enable Work Package creation.</p>
<div style="margin-top: 2rem; padding: 2rem; background: var(--bg); border-radius: 8px; border: 2px dashed var(--border);">
<h3 style="color: var(--text-dim);">SOP Status: <span id="sop-status">Not started</span></h3>
<p id="sop-progress-text" style="color: var(--text-light); margin-top: 0.5rem;"></p>
</div>
<p style="color: var(--text-light); margin: 1rem 0;">Complete the SOP Configuration first to enable Work Package creation. Once the SOP is finished, the full creator loads here with your project defaults pre-populated.</p>
<button class="nav-btn primary" onclick="switchTool('sop')" style="margin-top: 1rem;">← Go to SOP Configuration</button>
</div>
<!-- The real Work Package Creator, embedded once the SOP is complete -->
<iframe id="wp-frame" title="Work Package Creator" style="display:none; width:100%; border:0; min-height: calc(100vh - 200px);"></iframe>
</div>
</div>
</div>
<!-- STEP COMMENTS DROPDOWN (toggled from header) -->
<div id="comments-panel" class="comments-dropdown" style="display: none;">
<div class="comments-dropdown-header">
<strong>💬 Step Comments</strong>
<button onclick="toggleComments()" class="comments-dropdown-close" title="Close"></button>
</div>
<div style="margin-bottom: 1rem;">
<label style="font-weight: 600; font-size: 13px;">Your Name (optional)</label>
<input type="text" id="commenter-name" placeholder="e.g., Bill Clarida" style="width: 100%; padding: 0.5rem; border: 1px solid var(--border); border-radius: 4px; margin-top: 0.25rem;">
</div>
<div style="margin-bottom: 1rem;">
<label style="font-weight: 600; font-size: 13px;">Feedback</label>
<textarea id="comment-text" rows="3" placeholder="Your feedback here..." style="width: 100%; padding: 0.5rem; border: 1px solid var(--border); border-radius: 4px; margin-top: 0.25rem; font-family: inherit;"></textarea>
</div>
<button onclick="submitComment()" style="background: var(--primary); color: white; padding: 0.5rem 1rem; border: none; border-radius: 4px; cursor: pointer; font-weight: 600;">Submit</button>
<div id="comments-list" style="margin-top: 1rem; max-height: 240px; overflow-y: auto;"></div>
</div>
<!-- CONSTRAINT LIBRARY MODAL -->
<div id="constraint-modal" class="modal" style="display: none;">
<div class="modal-content">

View File

@@ -669,7 +669,20 @@ document.querySelectorAll('#status-group .radio-pill').forEach(p=>p.addEventList
// ── BOOT ─────────────────────────────────────────────────────────────────────
loadStore();
loadSampleSOP();
(function bootSOP(){
// When embedded in the Suite, hide the SOP import/sample controls (SOP is injected)
// and prefer the SOP the Suite just completed (persisted to localStorage).
const params = new URLSearchParams(location.search);
if(params.get('embedded')) document.body.classList.add('embedded');
try {
const raw = localStorage.getItem('wp_suite_sop');
if(raw){
const d = JSON.parse(raw);
if(d && d.woTypes){ SOP = d; applySOP(); newPackage(); return; }
}
} catch(e){}
loadSampleSOP();
})();
setRadio('status','Draft');
renderSavedList();
cmtInit();

View File

@@ -18,10 +18,10 @@
</div>
<div class="header-sep">|</div>
<div class="header-title">Work Package (IWP)</div>
<button class="btn btn-ghost" style="margin-left:auto;padding:7px 16px" onclick="document.getElementById('sop-import').click()">⤒ Import SOP</button>
<button class="btn btn-ghost embed-hide" style="margin-left:auto;padding:7px 16px" onclick="document.getElementById('sop-import').click()">⤒ Import SOP</button>
<input type="file" id="sop-import" accept="application/json" style="display:none" onchange="importSOP(event)">
<button class="btn btn-ghost" style="padding:7px 16px" onclick="loadSampleSOP()">⤓ Sample SOP</button>
<button class="btn btn-ghost" style="padding:7px 16px" onclick="openSopModal()">👁 View SOP</button>
<button class="btn btn-ghost embed-hide" style="padding:7px 16px" onclick="loadSampleSOP()">⤓ Sample SOP</button>
<button class="btn btn-ghost embed-first" style="padding:7px 16px" onclick="openSopModal()">👁 View SOP</button>
<button class="btn btn-ghost" style="padding:7px 16px" onclick="loadExample()">★ Load Example</button>
<button class="btn btn-ghost" style="padding:7px 16px" onclick="newPackage()">+ New</button>
<button class="btn btn-ghost" id="comments-btn" style="padding:7px 16px" onclick="toggleComments()">💬 Comments <span class="cbadge-total" id="cbadge-total" style="display:none">0</span></button>

View File

@@ -1,5 +1,9 @@
@import url('https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;500;600&family=IBM+Plex+Sans:ital,wght@0,300;0,400;0,500;0,600;0,700;1,300&display=swap');
/* Embedded-in-Suite tweaks */
body.embedded .embed-hide { display: none !important; }
body.embedded .embed-first { margin-left: auto; }
:root {
--bg: #f4f5f7;
--surface: #ffffff;