// ── GLOBAL STATE ────────────────────────────────────────────────────────────── let currentTool = 'sop'; let currentStep = 1; let sopComplete = false; let allComments = []; // Per-project storage key: 'wp_suite_sop' → 'wp_suite_sop__' when a // project is active. Keeps each project's SOP separate in the browser. function SK(base){ try { return (typeof ProjectData !== 'undefined' && ProjectData.key) ? ProjectData.key(base) : base; } catch(e){ return base; } } let state = { project: {name:'', number:'', client:'', division:'', site:''}, team: {pm:'', apm:'', cm:'', qm:''}, teamMembers: [], signoffRoles: [{role:'Superintendent',name:''},{role:'Foreman',name:''}], wpTypes: [], governance: {woformat:'', wosize:'', issuance:[], disciplines:['Mechanical','Electrical','Tech'], discMode:'choice', instanceSuffix:'letter', sizeHoursMax:''}, quality: {qcreq:'', photo:'', hold:''}, platforms: {tracking:'CxAlloy', commissioning:'CxAlloy'}, sequence: [], constraints: [], sources: [] }; let sop = null; // Generated SOP for WP tool // ── LIBRARIES ───────────────────────────────────────────────────────────────── 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 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 = [ 'Assistant Project Manager', '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(); renderTeamMembers(); renderOptionalRoles(); renderStandardConstraints(); renderSequenceSteps(); renderSources(); // Resolve the active project FIRST so per-project storage keys are correct // before we restore this project's SOP. const params = new URLSearchParams(window.location.search); applyProjectContext(params.get('project')); restoreSavedSOP(); updateStepUI(); updateProjectDisplay(); // Deep-link: ?tab=sop | ?tab=wp | ?view=dashboard from the home page. const tab = params.get('tab'); if(params.get('view') === 'dashboard') switchTool('wp'); else 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,notes:'',approval:''})); renderWPTypes(); } // ── LOAD SAMPLE DATA ────────────────────────────────────────────────────────── function loadSampleData(){ // Populate Step 1 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 = 'Mariano Sanchez'; document.getElementById('proj_apm').value = 'Assistant PM'; 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]-[TYPE]'; document.getElementById('gov_wosize').value = '3–5 days / 40–80 hours'; document.getElementById('gov_disciplines').value = 'Mechanical, Electrical, Tech'; document.getElementById('gov_discmode').value = 'choice'; document.getElementById('gov_size_hours_max').value = '120'; // 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(); 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 currentStep = 1; updateStepUI(); updateProjectDisplay(); } // ── RESTORE A COMPLETED SOP (for "Review" + WP tab across reloads) ───────────── function restoreSavedSOP(){ let savedState = null, savedSop = null, complete = false; try { complete = localStorage.getItem(SK('wp_suite_sop_complete')) === '1'; savedState = JSON.parse(localStorage.getItem(SK('wp_suite_state')) || 'null'); savedSop = JSON.parse(localStorage.getItem(SK('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('gov_disciplines', (state.governance.disciplines||[]).join(', ')); set('gov_discmode', state.governance.discMode); set('gov_size_hours_max', state.governance.sizeHoursMax); 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; // Update nav tabs document.querySelectorAll('.nav-tab').forEach(t=>t.classList.remove('active')); document.querySelector(`[data-tab="${tool}"]`).classList.add('active'); // Update content document.querySelectorAll('.tool').forEach(t=>t.classList.remove('active')); document.getElementById(`tool-${tool}`).classList.add('active'); // Reset step counter if(tool === 'sop'){ document.getElementById('total-steps').textContent = '10'; }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. const sp = new URLSearchParams(window.location.search); const wantDash = sp.get('view') === 'dashboard'; const projId = sp.get('project') || (activeProject && activeProject.id) || ''; frame.src = 'wp-creation-index.html?embedded=1' + (wantDash ? '&view=dashboard' : '') + (projId ? '&project=' + encodeURIComponent(projId) : '') + '&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(); } // Active project comes from the home page (?project= + ProjectData.getActive()). // When the SOP's project fields are still empty, prefill them from the project // record so the SOP is authored against the chosen project. let activeProject = null; function applyProjectContext(projectId){ try { if(typeof ProjectData !== 'undefined'){ if(projectId && ProjectData.getActiveId() !== projectId){ // Deep-linked to a project that isn't the cached active one. Seed the id // immediately so namespaced storage keys resolve, then fetch the full record. const cached = ProjectData.getActive(); ProjectData.setActive(cached && cached.id === projectId ? cached : { id: projectId }); ProjectData.get(projectId).then(p => { if(p){ activeProject = p; ProjectData.setActive(p); prefillProjectFields(); updateProjectDisplay(); } }); } activeProject = ProjectData.getActive(); } } catch(e){} prefillProjectFields(); } function prefillProjectFields(){ if(!activeProject) return; const set = (id,v)=>{ const el=document.getElementById(id); if(el && !el.value && v) el.value = v; }; set('proj_name', activeProject.name); set('proj_number', activeProject.number); set('proj_client', activeProject.client); set('proj_division', activeProject.division); set('proj_site', activeProject.site); if(typeof state !== 'undefined' && state.project){ state.project.name = state.project.name || activeProject.name || ''; state.project.number = state.project.number || activeProject.number || ''; state.project.client = state.project.client || activeProject.client || ''; state.project.division = state.project.division || activeProject.division || ''; state.project.site = state.project.site || activeProject.site || ''; } } function updateProjectDisplay(){ const projName = document.getElementById('proj_name')?.value || (activeProject && activeProject.name) || 'Project'; const display = document.getElementById('project-display'); if(display) display.textContent = sopComplete ? `✓ ${projName} (SOP Ready)` : projName; } // ── RENDERING (SOP) ──────────────────────────────────────────────────────────── function renderWPTypes(){ const container = document.getElementById('wp-types-table'); container.innerHTML = `
Work Order Type
Enabled
Special Rules / Notes
WO Complete Approval
`; state.wpTypes.forEach((t,i)=>{ const row = document.createElement('div'); row.className = 'wp-type-row'; row.innerHTML = `
${t.name}
`; container.appendChild(row); }); } function toggleWPType(i){ state.wpTypes[i].enabled = !state.wpTypes[i].enabled; renderWPTypes(); } function renderTeamMembers(){ const container = document.getElementById('team-members-list'); if(!container) return; container.innerHTML = state.teamMembers.map((m,i)=>`
`).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'); container.innerHTML = current.map((r,i)=>`
`).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=>`
${c.description}
`).join(''); 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=>`
${c}
Click to add to this project
`).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(); } 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(!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 ? `◆ HOLD` : `${stepNo}`; row.innerHTML = ` ${badge} `; 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), // so attribute values must be escaped or a re-render corrupts the field. function escAttr(v){ return String(v==null?'':v).replace(/&/g,'&').replace(//g,'>').replace(/"/g,'"'); } function renderSources(){ const container = document.getElementById('sources-list'); 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)=>`
`).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(); } } function previousStep(){ if(currentStep > 1){ currentStep--; updateStepUI(); } } 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'); // 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('sop-prev-btn').disabled = currentStep === 1; document.getElementById('sop-next-btn').style.display = currentStep < 10 ? 'block' : 'none'; document.getElementById('sop-complete-btn').style.display = currentStep === 10 ? 'block' : 'none'; // Collect form data 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.apm = document.getElementById('proj_apm').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); state.governance.disciplines = (document.getElementById('gov_disciplines').value||'') .split(',').map(d=>d.trim()).filter(Boolean); state.governance.discMode = document.getElementById('gov_discmode').value; state.governance.sizeHoursMax = document.getElementById('gov_size_hours_max').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; 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; } // ── SOP COMPLETION ──────────────────────────────────────────────────────────── function completeSOP(){ if(!validateStep(10)) return; collectStepData(); 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, apm: state.team.apm, cm: state.team.cm, qm: state.team.qm, site: state.project.site, teamMembers: state.teamMembers.filter(m=>(m.role||m.name)) }, 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, disciplines: (state.governance.disciplines && state.governance.disciplines.length) ? state.governance.disciplines : ['Mechanical','Electrical','Tech'], discMode: state.governance.discMode || 'choice', instanceSuffix: state.governance.instanceSuffix || 'letter', sizeHoursMax: state.governance.sizeHoursMax || '' }, woTypes: state.wpTypes.filter(t=>t.enabled).map(t=>({ name: t.name, enabled: true, notes: t.notes || '', approval: t.approval || '' })), 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 || ''})) }; sopComplete = true; updateProjectDisplay(); // Persist for the home page (green / "Review") and for the WP Creator tab, // namespaced to the active project so each project keeps its own SOP. try { localStorage.setItem(SK('wp_suite_sop'), JSON.stringify(sop)); localStorage.setItem(SK('wp_suite_state'), JSON.stringify(state)); localStorage.setItem(SK('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.'); // Hand the SOP to the embedded Work Package Creator and unlock its tab. if(typeof onSOPReady === 'function') onSOPReady(sop); } // ── 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('wp_suite_comments', JSON.stringify(allComments)); if(window.postFeedback) window.postFeedback({type:'sop_step_comment', ...comment}); document.getElementById('comment-text').value = ''; document.getElementById('commenter-name').value = ''; loadStepComments(); alert('✓ Comment submitted!'); } function exportComments(){ const saved = localStorage.getItem('wp_suite_comments'); const data = saved ? JSON.parse(saved) : []; if(!data.length){ alert('No comments to export yet.'); return; } const payload = {app:'Work Package Suite', source:'sop', exportedAt:new Date().toISOString(), comments:data}; const blob = new Blob([JSON.stringify(payload,null,2)], {type:'application/json'}); const a = document.createElement('a'); a.href = URL.createObjectURL(blob); a.download = 'wp-suite-comments-sop-' + new Date().toISOString().slice(0,10) + '.json'; a.click(); setTimeout(()=>URL.revokeObjectURL(a.href), 1000); } function importComments(ev){ const f = ev.target.files && ev.target.files[0]; if(!f) return; const r = new FileReader(); r.onload = ()=>{ try{ const inc = JSON.parse(r.result); const incoming = Array.isArray(inc) ? inc : (inc.comments || []); if(!incoming.length){ alert('No comments found in that file.'); return; } const saved = localStorage.getItem('wp_suite_comments'); allComments = saved ? JSON.parse(saved) : []; const seen = new Set(allComments.map(c=>c.step+'|'+c.timestamp+'|'+c.text)); let added = 0; incoming.forEach(c=>{ const k=c.step+'|'+c.timestamp+'|'+c.text; if(c.text && !seen.has(k)){ allComments.push(c); seen.add(k); added++; }}); localStorage.setItem('wp_suite_comments', JSON.stringify(allComments)); loadStepComments(); alert('Imported ' + added + ' comment' + (added===1?'':'s') + '.'); }catch(e){ alert('Could not read that file.'); } ev.target.value = ''; }; r.readAsText(f); } 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 = '
No comments yet on this step.
'; }else{ list.innerHTML = stepComments.map(c=>`
${c.name} • ${c.timestamp}
${c.text.replace(//g,'>')}
`).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'); }