diff --git a/html/help.js b/html/help.js new file mode 100644 index 0000000..4597c61 --- /dev/null +++ b/html/help.js @@ -0,0 +1,80 @@ +/* Shared Help + tooltip module for the Work Package Suite. + Included by the home page, the suite, and the embedded creator. It injects: + - tooltip styles for the .help-tip (ⓘ) component and [data-tip] hovers + - a Help modal (workflow + key concepts) opened via window.openHelp() + Add a "❔ Help" button anywhere with onclick="openHelp()". */ +(function (global) { + 'use strict'; + + var css = ` + .help-tip{ display:inline-flex; align-items:center; justify-content:center; width:15px; height:15px; + margin-left:5px; border-radius:50%; background:#5a6675; color:#fff; font-size:10px; font-weight:700; + font-family:ui-sans-serif,system-ui,sans-serif; cursor:help; vertical-align:middle; position:relative; } + .help-tip::after{ content:attr(data-tip); position:absolute; bottom:130%; left:50%; transform:translateX(-50%); + background:#1a2230; color:#fff; padding:7px 10px; border-radius:6px; font-size:12px; font-weight:400; + line-height:1.4; white-space:normal; width:max-content; max-width:260px; text-align:left; z-index:9999; + opacity:0; pointer-events:none; transition:opacity .12s; box-shadow:0 4px 14px rgba(20,30,50,.22); } + .help-tip::before{ content:''; position:absolute; bottom:130%; left:50%; transform:translate(-50%,95%); + border:5px solid transparent; border-top-color:#1a2230; opacity:0; transition:opacity .12s; z-index:9999; } + .help-tip:hover::after, .help-tip:hover::before, .help-tip:focus::after, .help-tip:focus::before{ opacity:1; } + + .ui-help-overlay{ position:fixed; inset:0; background:rgba(20,30,50,.5); display:none; align-items:flex-start; + justify-content:center; z-index:10000; padding:5vh 16px; overflow:auto; } + .ui-help-overlay.open{ display:flex; } + .ui-help-modal{ background:#fff; color:#1a2230; max-width:680px; width:100%; border-radius:10px; + box-shadow:0 12px 40px rgba(20,30,50,.3); font-family:ui-sans-serif,system-ui,-apple-system,'Segoe UI',sans-serif; } + .ui-help-head{ display:flex; align-items:center; justify-content:space-between; padding:16px 20px; + border-bottom:1px solid #e3e6ec; font-size:16px; } + .ui-help-head button{ background:none; border:none; font-size:18px; cursor:pointer; color:#5a6675; line-height:1; } + .ui-help-body{ padding:18px 22px; font-size:13.5px; line-height:1.6; } + .ui-help-body h4{ margin:18px 0 6px; font-size:13px; text-transform:uppercase; letter-spacing:.03em; color:#2563d6; } + .ui-help-body h4:first-child{ margin-top:0; } + .ui-help-body ol, .ui-help-body ul{ margin:0 0 6px; padding-left:20px; } + .ui-help-body li{ margin-bottom:5px; } + .ui-help-body code{ background:#f0f2f5; padding:1px 5px; border-radius:4px; font-size:12px; } + `; + var style = document.createElement('style'); + style.textContent = css; + (document.head || document.documentElement).appendChild(style); + + var HELP_HTML = ` +

How the suite works

+
    +
  1. Pick or create a Project on the home page — projects are stored centrally and each keeps its own SOP and Work Packages.
  2. +
  3. SOP Configuration — set the project baseline (team, sign-offs, WP types, governance & sizing, quality, sequence, constraints, sources). Every Work Package inherits these defaults.
  4. +
  5. Work Package Creation — author individual IWPs against the SOP. Use New for a blank one or Duplicate to copy an existing one.
  6. +
  7. Dashboard — track status, hours, and what's gating each package across the project.
  8. +
+ +

Key concepts

+ + +

Tips

+ `; + + function buildModal() { + if (document.getElementById('ui-help-overlay')) return; + var overlay = document.createElement('div'); + overlay.className = 'ui-help-overlay'; + overlay.id = 'ui-help-overlay'; + overlay.innerHTML = ''; + overlay.addEventListener('click', function (e) { if (e.target === overlay) closeHelp(); }); + document.body.appendChild(overlay); + } + + global.openHelp = function () { buildModal(); document.getElementById('ui-help-overlay').classList.add('open'); }; + global.closeHelp = function () { var o = document.getElementById('ui-help-overlay'); if (o) o.classList.remove('open'); }; + document.addEventListener('keydown', function (e) { if (e.key === 'Escape') global.closeHelp(); }); +})(window); diff --git a/html/index.html b/html/index.html index 5967045..d258f44 100644 --- a/html/index.html +++ b/html/index.html @@ -386,6 +386,7 @@ @@ -475,6 +476,7 @@ + + diff --git a/html/wp-creation-app.js b/html/wp-creation-app.js index 68235f2..f98b895 100644 --- a/html/wp-creation-app.js +++ b/html/wp-creation-app.js @@ -539,6 +539,7 @@ function updateReleaseBanner(){ else if(r.ready){ cls='rb-ready'; txt=`✓ Release-ready — all ${r.total} constraints cleared or N/A.`; } else { cls='rb-notready'; txt=`⚠ Not release-ready — ${r.open} of ${r.total} constraint${r.open===1?'':'s'} still open.`; } b.innerHTML=`
${txt}
`; + updateStickyStatus(); } function onStatusChange(target){ const idx=STATUS_ORDER.indexOf(target); @@ -761,8 +762,37 @@ function printPackage(){ // ── VIEWS ──────────────────────────────────────────────────────────────────── function hideDashboard(){ const dv=document.getElementById('dashboard-view'); if(dv) dv.style.display='none'; } -function showOutput(){ hideDashboard(); document.querySelectorAll('.main > .card, .main > .nav-row').forEach(e=>e.style.display='none'); document.getElementById('pkg-output').style.display=''; renderSavedList(); currentView='Package View'; window.scrollTo({top:0,behavior:'smooth'}); } -function showForm(){ hideDashboard(); document.querySelectorAll('.main > .card').forEach(e=>e.style.display=''); document.querySelector('.main > .nav-row').style.display='flex'; document.getElementById('pkg-output').style.display='none'; document.getElementById('saved-card').style.display = savedPackages.length?'':'none'; buildDisciplinePicker(); renderScope(); currentView='Work Package Form'; window.scrollTo({top:0,behavior:'smooth'}); } +function showOutput(){ hideDashboard(); setFormChrome(false); document.querySelectorAll('.main > .card, .main > .nav-row').forEach(e=>e.style.display='none'); document.getElementById('pkg-output').style.display=''; renderSavedList(); currentView='Package View'; window.scrollTo({top:0,behavior:'smooth'}); } +function showForm(){ hideDashboard(); document.querySelectorAll('.main > .card').forEach(e=>e.style.display=''); document.querySelector('.main > .nav-row').style.display='flex'; document.getElementById('pkg-output').style.display='none'; document.getElementById('saved-card').style.display = savedPackages.length?'':'none'; buildDisciplinePicker(); renderScope(); setFormChrome(true); currentView='Work Package Form'; window.scrollTo({top:0,behavior:'smooth'}); } + +// Sticky save bar + section-nav chrome (shown only on the editable form view). +function setFormChrome(on){ + const nav=document.getElementById('section-nav'), save=document.getElementById('sticky-save'); + if(nav) nav.style.display = on ? '' : 'none'; + if(save) save.style.display = on ? 'flex' : 'none'; + document.body.classList.toggle('has-sticky-save', !!on); + if(on){ buildSectionNav(); updateStickyStatus(); } +} +function buildSectionNav(){ + const nav=document.getElementById('section-nav'); if(!nav) return; + const chips=[]; + document.querySelectorAll('.main > .card').forEach((card,i)=>{ + if(card.id==='saved-card' || card.style.display==='none') return; + const h=card.querySelector('.section-title, .sub-heading'); if(!h) return; + const clone=h.cloneNode(true); clone.querySelectorAll('.help-tip').forEach(x=>x.remove()); + const label=clone.textContent.trim().replace(/\s+/g,' '); if(!label) return; + if(!card.id) card.id='sec-'+i; + chips.push(`${esc(label)}`); + }); + nav.innerHTML=chips.join(''); +} +function updateStickyStatus(){ + const el=document.getElementById('sticky-status'); if(!el) return; + const r=readiness(); const st=getRadio('status'); + if(st==='Issue'){ el.className='sticky-status ss-hold'; el.textContent=`⛔ On hold — ${r.open} constraint${r.open===1?'':'s'} reopened`; } + else if(r.ready){ el.className='sticky-status ss-ready'; el.textContent=`✓ Release-ready — all ${r.total} constraints cleared`; } + else { el.className='sticky-status ss-notready'; el.textContent=`⚠ ${r.open} of ${r.total} constraint${r.open===1?'':'s'} open`; } +} // ── SAVED PACKAGES ─────────────────────────────────────────────────────────── const STORE_KEY='wp_iwp_v1'; @@ -780,7 +810,7 @@ function renderSavedList(){ const tag = p.split?' master':(p.instanceOf?` ${esc(p.instanceLabel||'instance')}`:''); const disc = (p.disciplines&&p.disciplines.length)?`
${esc(p.disciplines.join(', '))}
`:''; return `${esc(p.number||'—')}${tag}${disc}${esc(p.type||'')}${esc(p.subject||'')} - ${esc(p.status||'')}${ready} + ${statusPill(p.status)}${ready} `; }).join(''); } @@ -898,13 +928,26 @@ const WPData = { p.status=status; p.updatedAt=new Date().toISOString(); saveStore(); return true; }, // → POST /api/wps/{id}/status }; -let dashFilter={status:'',discipline:'',q:''}; +let dashFilter={status:'',discipline:'',q:'',flag:''}; +function dashToggleFlag(f){ + if(f==='all'){ dashFilter={status:'',discipline:'',q:'',flag:''}; } + else { dashFilter.flag = dashFilter.flag===f ? '' : f; } + renderDashboard(); +} +function dashSetStatus(s){ dashFilter.status = dashFilter.status===s ? '' : s; renderDashboard(); } +// Consistent colored status pill, reused by the dashboard board and the saved list. +function statusPill(s){ + const map={'Draft':'badge-NA','Scheduled':'badge-O','Issued':'badge-Y','In Progress':'badge-O','QC':'badge-O','Closed':'badge-Y','Issue':'badge-N'}; + const label = s==='Issue' ? 'Issue (Hold)' : (s||'—'); + return `${esc(label)}`; +} function wpOpenConstraints(p){ return (p.constraints||[]).filter(c=>c.status==='open'); } function isOverdue(p){ return !!(p.due && p.status!=='Closed' && p.due < todayStr()); } // Masters are roll-ups of their instances — exclude them from counts so work isn't double-counted. function countableWPs(){ return WPData.list().filter(p=>!p.split); } function showDashboard(){ + setFormChrome(false); document.querySelectorAll('.main > .card, .main > .nav-row').forEach(e=>e.style.display='none'); document.getElementById('pkg-output').style.display='none'; const dv=document.getElementById('dashboard-view'); if(dv) dv.style.display=''; @@ -923,19 +966,25 @@ function renderDashboard(){ if(isOverdue(p)) overdue++; (p.disciplines&&p.disciplines.length?p.disciplines:['(none)']).forEach(d=>byDisc[d]=(byDisc[d]||0)+1); }); - const card=(label,val,cls)=>`
${val}
${esc(label)}
`; + // Clickable metric cards filter the board (flag-based); a card with no flag is static. + const card=(label,val,cls,flag)=>{ + const active = flag && dashFilter.flag===flag ? ' dm-active' : ''; + const attr = flag ? ` onclick="dashToggleFlag('${flag}')" title="Click to filter the board"` : ''; + return `
${val}
${esc(label)}
`; + }; let h=`
- ${card('Total WPs', all.length)} - ${card('Release-ready', ready, ready?'dm-green':'')} - ${card('On hold', hold, hold?'dm-red':'')} - ${card('Overdue', overdue, overdue?'dm-red':'')} + ${card('Total WPs', all.length, '', 'all')} + ${card('Release-ready', ready, ready?'dm-green':'', 'ready')} + ${card('On hold', hold, hold?'dm-red':'', 'onhold')} + ${card('Overdue', overdue, overdue?'dm-red':'', 'overdue')} ${card('Est. hrs', Math.round(estH))} ${card('Actual hrs', Math.round(actH))}
`; - // status + discipline breakdown chips - const statusChips=STATUS_ORDER.filter(s=>byStatus[s]).map(s=>`${esc(s)}: ${byStatus[s]}`).join('') - + (byStatus['Issue']?`On Hold: ${byStatus['Issue']}`:''); + // status + discipline breakdown chips (status chips also filter the board) + const statusChip=(label,count,cls,status)=>`${esc(label)}: ${count}`; + const statusChips=STATUS_ORDER.filter(s=>byStatus[s]).map(s=>statusChip(s,byStatus[s],'',s)).join('') + + (byStatus['Issue']?statusChip('On Hold',byStatus['Issue'],'chip-red','Issue'):''); const discChips=Object.keys(byDisc).map(d=>`${esc(d)}: ${byDisc[d]}`).join('')||''; h+=`
By status
${statusChips||'—'}
By discipline
${discChips}
`; @@ -965,6 +1014,9 @@ function renderDashboard(){ if(dashFilter.status && p.status!==dashFilter.status) return false; if(dashFilter.discipline && !((p.disciplines||[]).includes(dashFilter.discipline))) return false; if(q && !((p.number||'')+' '+(p.subject||'')).toLowerCase().includes(q)) return false; + if(dashFilter.flag==='ready' && !(!p.split && wpOpenConstraints(p).length===0 && p.status!=='Closed' && p.status!=='Issue')) return false; + if(dashFilter.flag==='onhold' && p.status!=='Issue') return false; + if(dashFilter.flag==='overdue' && !isOverdue(p)) return false; return true; }); h+=`
Work Packages (${rows.length})
@@ -980,7 +1032,7 @@ function renderDashboard(){ h+=`${esc(p.number||'—')}${p.instanceOf?` ${esc(p.instanceLabel||'')}`:''} ${esc(p.subject||'')}${esc(p.type||'')} ${esc((p.disciplines||[]).join(', '))||ns()} - ${esc(p.status||'')}${gates}${due}${cell(p.hours)} + ${statusPill(p.status)}${gates}${due}${cell(p.hours)} ${issueBtn} `; }); h+=`
`; diff --git a/html/wp-creation-index.html b/html/wp-creation-index.html index e684e1e..08e959e 100644 --- a/html/wp-creation-index.html +++ b/html/wp-creation-index.html @@ -38,6 +38,9 @@
+ +
+
@@ -45,7 +48,7 @@
General Information
Parameters in blue are inherited from the project SOP. Fill the rest for this package.
-
+
@@ -88,14 +91,14 @@
-
Scope & Work
+
Scope & Worki
Enter the work as ordered steps — added in sequence, the way the crew performs them.
@@ -113,7 +116,7 @@
-
Material List
+
Material Listi
Structured bill of materials. Feeds kitting and the delivery forecast. Unit is from the Acumatica unit list.
QtyUnitDescription
@@ -157,7 +160,7 @@
-
Constraints — Release Readiness
+
Constraints — Release Readinessi
Per AWP, a package is not released to the field until every constraint is Cleared or N/A. If a constraint reopens after release, status drops to Issue (Hold).
ConstraintStatusComment
@@ -276,8 +279,18 @@
+ + + + diff --git a/html/wp-creation-styles.css b/html/wp-creation-styles.css index 92953ac..9c84d05 100644 --- a/html/wp-creation-styles.css +++ b/html/wp-creation-styles.css @@ -564,6 +564,26 @@ .so-date { font-size:13px; font-variant-numeric:tabular-nums; } .so-ovr { margin-left:8px; font-size:11px; } + /* Section nav (jump chips) */ + .section-nav-bar{ position:sticky; top:0; z-index:30; display:flex; flex-wrap:wrap; gap:6px; + padding:8px 12px; background:rgba(255,255,255,.92); backdrop-filter:blur(4px); + border-bottom:1px solid var(--border); } + .section-nav-bar:empty{ display:none; } + .sec-chip{ font-size:12px; font-weight:600; color:var(--text-muted); background:var(--surface2); + border:1px solid var(--border); border-radius:14px; padding:4px 11px; cursor:pointer; white-space:nowrap; } + .sec-chip:hover{ border-color:var(--accent); color:var(--accent); } + + /* Sticky save bar */ + .sticky-save{ position:fixed; left:0; right:0; bottom:0; z-index:40; display:flex; align-items:center; + justify-content:space-between; gap:14px; padding:10px 20px; background:#fff; + border-top:1px solid var(--border-strong); box-shadow:0 -2px 10px rgba(20,30,50,.08); } + .sticky-save .sticky-status{ font-size:13px; font-weight:600; } + .sticky-save .sticky-actions{ display:flex; gap:10px; } + .ss-ready{ color:var(--accent-green); } + .ss-notready{ color:var(--accent-amber); } + .ss-hold{ color:var(--red); } + body.has-sticky-save .main{ padding-bottom:74px; } + /* Disciplines + per-discipline scope */ .disc-picker { display:flex; flex-wrap:wrap; gap:8px; } .disc-pill { display:flex; align-items:center; gap:7px; padding:7px 13px; border:1px solid var(--border-strong); @@ -586,6 +606,11 @@ .dash-metric .dm-label { font-size:11px; color:var(--text-muted); margin-top:6px; text-transform:uppercase; letter-spacing:.03em; } .dash-metric.dm-green .dm-val { color:var(--accent-green); } .dash-metric.dm-red .dm-val { color:var(--red); } + .dash-metric[onclick] { cursor:pointer; transition:border-color .12s, box-shadow .12s; } + .dash-metric[onclick]:hover { border-color:var(--accent); } + .dash-metric.dm-active { border-color:var(--accent); box-shadow:0 0 0 2px var(--accent-dim); } + .dash-chip[onclick] { cursor:pointer; } + .dash-chip.chip-active { border-color:var(--accent); color:var(--accent); background:var(--accent-dim); } .dash-breakdown { display:grid; grid-template-columns:1fr 1fr; gap:16px; margin-bottom:16px; } .dash-bd-title { font-size:11px; font-weight:700; text-transform:uppercase; color:var(--text-muted); margin-bottom:6px; } .dash-chip { display:inline-block; font-size:12px; background:var(--surface2); border:1px solid var(--border); border-radius:14px; padding:3px 10px; margin:0 6px 6px 0; }