diff --git a/html/admin.html b/html/admin.html index 9294b4f..153a3c9 100644 --- a/html/admin.html +++ b/html/admin.html @@ -103,6 +103,29 @@
+ +
+

All feedback & comments

+
Every comment submitted across the suite — who wrote it, what they said, and where they were (page & step) when they commented.
+
+ + + +
+
Click refresh to load.
+
+ + +
+

Usage logs

+
Engagement recorded by the suite — sessions, step views, and actions. Note: stored locally per browser, so this reflects activity on this machine.
+
+ + +
+
Click refresh to load.
+
+

Database snapshot

diff --git a/html/admin.js b/html/admin.js index 676eabd..d295263 100644 --- a/html/admin.js +++ b/html/admin.js @@ -11,6 +11,8 @@ function reveal(){ document.getElementById('admin-main').style.display=''; checkHealth(); loadUsers(); + loadComments(); + loadUsage(); } function showDenied(){ document.getElementById('admin-denied').style.display=''; @@ -195,6 +197,7 @@ function renderUsers(list, meId){ ''+(active?'active':'disabled')+''+ ''+fmt(u.last_login_at)+''+ '
'+ + ''+ ''+ disableBtn+delBtn+ '
'+ @@ -248,6 +251,125 @@ async function deleteUser(id, username){ else alert('Failed: '+((json && json.detail)||('HTTP '+status))); } +// ── project access assignment ─────────────────────────────────────────────────── +async function manageProjects(id, username){ + const { status, json } = await api('GET','/api/auth/users/'+id+'/projects'); + if(status!==200 || !json){ alert('Could not load projects (HTTP '+status+').'); return; } + openProjectModal(id, username, json.projects||[], new Set(json.assigned||[]), json.user); +} +function closeProjectModal(){ const m=document.getElementById('proj-modal'); if(m) m.remove(); } +function openProjectModal(userId, username, projects, assigned, userObj){ + closeProjectModal(); + const isAdmin = userObj && userObj.role==='admin'; + const items = projects.length ? projects.map(p => + '').join('') : '
No projects exist yet.
'; + const modal = document.createElement('div'); + modal.id = 'proj-modal'; + modal.style.cssText = 'position:fixed;inset:0;background:rgba(20,30,50,.5);display:flex;align-items:center;justify-content:center;z-index:10002;padding:20px;'; + modal.innerHTML = + '
'+ + '
Project access — '+uesc(username)+'
'+ + '
'+ + (isAdmin ? '' : '
Tick the projects this user may access.
')+ + '
'+items+'
'+ + '
'+ + '
'+ + ''+ + (isAdmin ? '' : '')+ + '
'+ + '
'; + modal.addEventListener('click', e => { if(e.target===modal) closeProjectModal(); }); + document.body.appendChild(modal); + const saveBtn = document.getElementById('proj-save'); + if(saveBtn) saveBtn.onclick = async () => { + const ids = [...modal.querySelectorAll('#proj-list input[type=checkbox]:checked')].map(c=>c.value); + const { status } = await api('PUT','/api/auth/users/'+userId+'/projects',{project_ids:ids}); + if(status===200) closeProjectModal(); + else alert('Save failed (HTTP '+status+').'); + }; +} + +// ── all feedback / comments ───────────────────────────────────────────────────── +let _comments = []; +async function loadComments(){ + const box = document.getElementById('comments-admin'); + box.textContent = 'Loading…'; + const { status, json } = await api('GET','/api/comments'); + if(status!==200 || !Array.isArray(json)){ + box.innerHTML = ''; return; + } + _comments = json; + const sel = document.getElementById('cmt-filter'); const cur = sel.value; + const sources = [...new Set(json.map(c=>c.source).filter(Boolean))].sort(); + sel.innerHTML = '' + sources.map(s=>'').join(''); + sel.value = cur; + renderComments(); +} +function renderComments(){ + const box = document.getElementById('comments-admin'); + const src = document.getElementById('cmt-filter').value; + const q = (document.getElementById('cmt-search').value||'').toLowerCase(); + let rows = _comments.filter(c => (!src || c.source===src) && + (!q || ((c.text||'')+' '+(c.author||'')).toLowerCase().indexOf(q)>=0)); + if(!rows.length){ box.innerHTML = '
No comments'+((src||q)?' match the filter.':' yet.')+'
'; return; } + rows = rows.slice().sort((a,b)=> String(b.created_at||'').localeCompare(String(a.created_at||''))); + const fmt = s => s ? new Date(s).toLocaleString() : '—'; + const where = c => { + const bits = []; + if(c.page) bits.push(uesc(c.page)); + if(c.step!=null) bits.push('step '+c.step); + if(c.sop_id) bits.push('SOP '+uesc(c.sop_id)); + if(c.wp_id) bits.push('WP '+uesc(c.wp_id)); + return bits.join(' · ') || '—'; + }; + box.innerHTML = ''+ + rows.map(c => ''+ + ''+ + ''+ + ''+ + ''+ + ''+ + '').join('')+'
WhenWhoSourceWhereComment
'+fmt(c.created_at)+''+uesc(c.author||'Anonymous')+''+uesc(c.source||'—')+''+where(c)+''+uesc(c.text||'')+'
'; +} + +// ── usage logs (read from this browser's localStorage) ────────────────────────── +const USAGE_KEY = 'wp_suite_analytics_v1'; +function usageLoad(){ try { return JSON.parse(localStorage.getItem(USAGE_KEY)) || {events:[]}; } catch(e){ return {events:[]}; } } +function loadUsage(){ + const box = document.getElementById('usage-admin'); + const evs = (usageLoad().events) || []; + if(!evs.length){ box.innerHTML = '
No usage recorded in this browser yet.
'; return; } + const byEvent = {}, byStep = {}, sessions = new Set(); + let first = evs[0].ts, last = evs[0].ts; + evs.forEach(e => { + byEvent[e.event] = (byEvent[e.event]||0)+1; + if(e.session) sessions.add(e.session); + if(e.event==='step_view' && e.detail) byStep[e.detail.step] = (byStep[e.detail.step]||0)+1; + if(e.ts < first) first = e.ts; if(e.ts > last) last = e.ts; + }); + const fmt = s => s ? new Date(s).toLocaleString() : '—'; + let html = ''+ + ''+ + ''+ + '
Sessions'+sessions.size+'
Events'+evs.length+'
Range'+fmt(first)+' → '+fmt(last)+'
'; + html += '

Step views

'; + for(let i=1;i<=10;i++) html += ''; + html += '
StepViews
Step '+i+''+(byStep[i]||0)+'
'; + html += '

Actions

'; + Object.keys(byEvent).sort().forEach(k => html += ''); + html += '
EventCount
'+uesc(k)+''+byEvent[k]+'
'; + box.innerHTML = html; +} +function downloadUsage(){ + const blob = new Blob([JSON.stringify(usageLoad(),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(); setTimeout(()=>URL.revokeObjectURL(a.href), 1000); +} + // ── access control: admins only ───────────────────────────────────────────────── // auth-guard.js requires a login and sets window.WP_USER (firing 'wp-auth-ready'). // Show the console for admins; otherwise show the "Admins only" notice. diff --git a/html/login.html b/html/login.html index 2eebbac..e155089 100644 --- a/html/login.html +++ b/html/login.html @@ -76,7 +76,6 @@
Prime Controls - Prime Controls

Sign in

Work Package Suite

diff --git a/html/project-data.js b/html/project-data.js index 10f285d..b6090b8 100644 --- a/html/project-data.js +++ b/html/project-data.js @@ -25,7 +25,7 @@ function cacheRemove(id) { writeLocal(readLocal().filter(function (x) { return x.id !== id; })); } var SAMPLE_PROJECT = { - name: 'Micron — INC Construction Work Packages', number: '26-67-008', + name: 'Micron FMCS Install (sample)', number: '26-67-008', client: 'Micron Technology, Inc.', division: 'Semiconductor', site: 'Boise, ID — Fab', sample: true }; diff --git a/html/work-package-suite-app.js b/html/work-package-suite-app.js index 69cf65a..205ea22 100644 --- a/html/work-package-suite-app.js +++ b/html/work-package-suite-app.js @@ -159,6 +159,12 @@ window.addEventListener('DOMContentLoaded',()=>{ else if(tab === 'wp' || tab === 'sop') switchTool(tab); track('app_open'); + + // Feedback author auto-populates from the signed-in user (auth-guard sets + // window.WP_USER and fires 'wp-auth-ready'); the field is read-only. + setCommenterName(); + document.addEventListener('wp-auth-ready', setCommenterName); + let _fieldTimer; document.addEventListener('input', e=>{ const t = e.target; @@ -853,8 +859,17 @@ function toggleComments(){ if(panel.style.display === 'block') loadStepComments(); } +function currentUserName(){ + const u = window.WP_USER; + return (u && (u.full_name || u.username)) || ''; +} +function setCommenterName(){ + const el = document.getElementById('commenter-name'); + if(el) el.value = currentUserName(); +} + function submitComment(){ - const name = document.getElementById('commenter-name').value || 'Anonymous'; + const name = document.getElementById('commenter-name').value || currentUserName() || 'Anonymous'; const text = document.getElementById('comment-text').value.trim(); if(!text){ alert('Please enter a comment.'); return; } diff --git a/html/work-package-suite.html b/html/work-package-suite.html index 9ec4568..9114c5b 100644 --- a/html/work-package-suite.html +++ b/html/work-package-suite.html @@ -23,10 +23,9 @@
- - - - + + + 1 / 10
@@ -34,13 +33,13 @@ @@ -298,7 +297,7 @@
- +
@@ -329,9 +328,9 @@
- - - + + +
@@ -342,9 +341,9 @@
-

📋 Work Package Creation

+

Work Package Creation

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.

- +
@@ -357,12 +356,12 @@