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:
@@ -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,'"')}" 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,'"')}" 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,'"')}" 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');
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user