Brings the Work Package Suite from a browser-local prototype to a multi-tenant, SQL-backed deployment hardened for customer IP. Auth & access control - Local username/password login (bcrypt + JWT in an HttpOnly cookie), admin-managed users, per-project membership, and project-scoped API access. - Admin console: change user roles, view the audit trail, manage settings. Security hardening - CSP / HSTS / X-Frame-Options / nosniff headers in nginx; Secure cookie via X-Forwarded-Proto; CSRF Origin check; attribute-safe output escaping. - Login lockout, token_version session revocation, stronger password policy, fail-closed secret loading, encrypted (AES-256) database backups. Persistence & schema - SOPs and Work Packages are now DB-backed and shared across users, written through a durable client sync outbox that queues offline edits. - Alembic migrations applied automatically on container start. New capabilities - Phase 2 dashboard (progress, gating, pagination, archive). - Phase 3 PWA "Field View" with offline caching and auth fallback. - WP owner assignment with OPTIONAL email notifications, OFF by default and toggled from the admin console. SMTP password is read only from the SMTP_PASSWORD env var (never stored); emails carry a WP number + deep link, never customer IP. Also: IBM Carbon restyle, Help section, and DEPLOYMENT.md brought up to date. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
482 lines
35 KiB
JavaScript
482 lines
35 KiB
JavaScript
/* Shared Help center + 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 searchable, multi-topic Help center modal opened via window.openHelp()
|
||
- a floating "?" launcher on any page that doesn't already have a Help button
|
||
|
||
API (unchanged + extended):
|
||
openHelp() open the help center
|
||
openHelp('topicId') open and jump to a topic (e.g. openHelp('constraints'))
|
||
closeHelp() close it
|
||
Add a Help button anywhere with onclick="openHelp()". */
|
||
(function (global) {
|
||
'use strict';
|
||
|
||
// ── styles ────────────────────────────────────────────────────────────────
|
||
var css = `
|
||
.help-tip{ display:inline-flex; align-items:center; justify-content:center; width:15px; height:15px;
|
||
margin-left:5px; border-radius:50%; background:#525252; 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:#161616; color:#fff; padding:7px 10px; border-radius:0; 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:#161616; 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:center;
|
||
justify-content:center; z-index:10000; padding:4vh 16px; }
|
||
.ui-help-overlay.open{ display:flex; }
|
||
.ui-help-modal{ background:#fff; color:#161616; max-width:980px; width:100%; height:88vh; max-height:880px;
|
||
border-radius:0; box-shadow:0 12px 40px rgba(20,30,50,.3); display:flex; flex-direction:column; overflow:hidden;
|
||
font-family:ui-sans-serif,system-ui,-apple-system,'Segoe UI',sans-serif; }
|
||
.ui-help-head{ display:flex; align-items:center; gap:14px; padding:13px 18px; border-bottom:1px solid #e0e0e0; flex:none; }
|
||
.ui-help-head .ui-help-title{ font-size:15px; font-weight:700; white-space:nowrap; }
|
||
.ui-help-search{ flex:1; position:relative; max-width:420px; }
|
||
.ui-help-search input{ width:100%; padding:8px 12px; border:1px solid #8d8d8d; border-radius:0;
|
||
font-size:13px; outline:none; background:#f7f8fa; }
|
||
.ui-help-search input:focus{ border-color:#0f62fe; background:#fff; box-shadow:0 0 0 2px rgba(37,99,214,.15); }
|
||
.ui-help-head .ui-help-x{ margin-left:auto; background:none; border:none; font-size:20px; cursor:pointer; color:#525252; line-height:1; }
|
||
.ui-help-wrap{ display:flex; flex:1; min-height:0; }
|
||
.ui-help-nav{ width:230px; flex:none; border-right:1px solid #e0e0e0; overflow:auto; padding:10px 8px; background:#fafbfc; }
|
||
.ui-help-nav a{ display:block; padding:7px 10px; border-radius:0; color:#27313f; text-decoration:none; font-size:13px;
|
||
cursor:pointer; margin-bottom:1px; }
|
||
.ui-help-nav a:hover{ background:#eef1f6; }
|
||
.ui-help-nav a.active{ background:#edf5ff; color:#0353e9; font-weight:600; }
|
||
.ui-help-nav a.nohit{ display:none; }
|
||
.ui-help-content{ flex:1; overflow:auto; padding:22px 28px; scroll-behavior:smooth; }
|
||
.ui-help-sec{ margin-bottom:30px; }
|
||
.ui-help-sec.hide{ display:none; }
|
||
.ui-help-sec h3{ font-size:18px; margin:0 0 10px; color:#161616; scroll-margin-top:10px; }
|
||
.ui-help-sec h4{ margin:18px 0 6px; font-size:12px; text-transform:uppercase; letter-spacing:.04em; color:#0f62fe; }
|
||
.ui-help-content p{ font-size:13.5px; line-height:1.62; margin:0 0 9px; color:#27313f; }
|
||
.ui-help-content ol, .ui-help-content ul{ margin:0 0 10px; padding-left:20px; font-size:13.5px; line-height:1.6; }
|
||
.ui-help-content li{ margin-bottom:5px; }
|
||
.ui-help-content code{ background:#eef1f6; padding:1px 5px; border-radius:4px; font-size:12px; }
|
||
.ui-help-content table{ border-collapse:collapse; width:100%; font-size:12.5px; margin:6px 0 12px; }
|
||
.ui-help-content th, .ui-help-content td{ border:1px solid #e0e0e0; padding:6px 9px; text-align:left; vertical-align:top; }
|
||
.ui-help-content th{ background:#f4f6f9; font-weight:600; }
|
||
.ui-help-pill{ display:inline-block; padding:1px 8px; border-radius:11px; font-size:11px; font-weight:600; }
|
||
.pill-draft{ background:#eef1f6; color:#525252; } .pill-sched{ background:#edf5ff; color:#0353e9; }
|
||
.pill-prog{ background:#fef3e0; color:#b45309; } .pill-issued{ background:#e4f6ec; color:#15924f; }
|
||
.pill-qc{ background:#f3e8ff; color:#7c3aed; } .pill-closed{ background:#e2e8f0; color:#334155; }
|
||
.pill-hold{ background:#fde8e8; color:#c0392b; }
|
||
.ui-help-callout{ background:#f4f8ff; border-left:3px solid #0f62fe; padding:10px 14px; border-radius:0;
|
||
font-size:13px; line-height:1.55; margin:10px 0; }
|
||
.ui-help-noresult{ display:none; color:#525252; font-size:14px; padding:10px 2px; }
|
||
.ui-help-content mark{ background:#fff1a8; color:inherit; border-radius:2px; padding:0 1px; }
|
||
.ui-help-fab{ position:fixed; bottom:12px; left:12px; z-index:9998; width:38px; height:38px; border-radius:50%;
|
||
border:none; background:#0f62fe; color:#fff; font-size:18px; font-weight:700; cursor:pointer;
|
||
box-shadow:0 2px 10px rgba(20,30,50,.28); }
|
||
.ui-help-fab:hover{ background:#0353e9; }
|
||
@media (max-width:760px){
|
||
.ui-help-modal{ height:92vh; } .ui-help-wrap{ flex-direction:column; }
|
||
.ui-help-nav{ width:auto; display:flex; flex-wrap:wrap; gap:4px; border-right:none; border-bottom:1px solid #e0e0e0; }
|
||
.ui-help-nav a{ margin:0; font-size:12px; padding:5px 9px; }
|
||
.ui-help-head{ flex-wrap:wrap; }
|
||
}`;
|
||
var style = document.createElement('style');
|
||
style.textContent = css;
|
||
(document.head || document.documentElement).appendChild(style);
|
||
|
||
// ── content ─────────────────────────────────────────────────────────────────
|
||
// Each topic: { id, title, body(HTML) }. Order here is the nav order.
|
||
var TOPICS = [
|
||
{ id: 'overview', title: 'Getting started', body: `
|
||
<h3>Getting started</h3>
|
||
<p>The Work Package Suite turns a project's standard procedure into release-ready <strong>Installation Work Packages (IWPs)</strong>. You work in three stages, always in the same order:</p>
|
||
<ol>
|
||
<li><strong>Pick or create a Project</strong> on the home page. Each project keeps its own SOP and its own Work Packages, so you can run many jobs at once.</li>
|
||
<li><strong>SOP Configuration</strong> — set the project baseline in 10 steps (team, sign-offs, WP types, governance & sizing, quality, platforms, sequence, constraints, sources). Every Work Package inherits these defaults. The Creator stays locked until the SOP is marked complete.</li>
|
||
<li><strong>Work Package Creation</strong> — author individual IWPs against the SOP, clear their constraints, and issue them to the field.</li>
|
||
<li><strong>Dashboard</strong> — track status, hours, due dates, and what's gating each package across the project.</li>
|
||
</ol>
|
||
<h4>Moving around</h4>
|
||
<p>From the home page, open <strong>SOP Configuration</strong>, the <strong>Work Package Creator</strong>, or the <strong>Dashboard</strong>. Inside the suite, switch any time using the top tabs: <strong>⚙️ SOP Configuration</strong>, <strong>📋 Work Package Creation</strong>, and <strong>📊 Dashboard</strong>. The active project and SOP follow you across all of them.</p>
|
||
<h4>Quick start</h4>
|
||
<ol>
|
||
<li><strong>Open “SOP Configuration”</strong> and complete the 10 steps for your project (~15 minutes).</li>
|
||
<li><strong>Finish the SOP</strong> — its home-page card turns green and unlocks the Work Package Creator.</li>
|
||
<li><strong>Open “Work Package Creation”</strong> to author packages with your SOP defaults pre-populated.</li>
|
||
<li><strong>Update from the field</strong> using the <strong>Field View</strong>, and <strong>leave feedback</strong> on any page with the Feedback button.</li>
|
||
</ol>
|
||
<div class="ui-help-callout">New here? On the home page choose the <strong>Sample Project</strong>, then click <strong>⭐ Load Sample</strong> in the suite to see a fully filled-out SOP and an example Work Package.</div>` },
|
||
|
||
{ id: 'projects', title: 'Projects', body: `
|
||
<h3>Projects</h3>
|
||
<p>A <strong>project</strong> is the top-level container — every SOP and Work Package belongs to one. Create or select projects on the home page.</p>
|
||
<h4>Project fields</h4>
|
||
<ul>
|
||
<li><strong>Project Name</strong> (required)</li>
|
||
<li><strong>Project Number</strong></li>
|
||
<li><strong>Client</strong></li>
|
||
<li><strong>Division / Sector</strong></li>
|
||
<li><strong>Site / Location</strong></li>
|
||
</ul>
|
||
<h4>The active project</h4>
|
||
<p>The <strong>active project</strong> is the one you're currently working in. All SOP and Work Package data is scoped (namespaced) to it, so switching projects loads that project's own configuration and packages — nothing leaks between jobs. Use the <em>change</em> link next to the active project name to switch.</p>
|
||
<div class="ui-help-callout">Projects are stored centrally via the API and mirrored to your browser, so the suite still works offline; it re-syncs when the connection returns.</div>` },
|
||
|
||
{ id: 'sop', title: 'SOP Configuration', body: `
|
||
<h3>SOP Configuration (10 steps)</h3>
|
||
<p>The SOP is the project baseline. Walk the 10 steps with <strong>← Back</strong> / <strong>Next →</strong>, or jump using the step indicators. The final step is <strong>✓ SOP Complete</strong> — saving it unlocks the Work Package Creator and turns the home-page card green.</p>
|
||
<ol>
|
||
<li><strong>Project Basics</strong> — name, number, client, division/sector, site. Inherited by every WP.</li>
|
||
<li><strong>Project Team Leadership</strong> — PM, APM, CM, QM, plus any additional members (<em>+ Add Team Member</em>).</li>
|
||
<li><strong>Required Sign-Off Roles</strong> — Superintendent and Foreman are always required; add optional roles (HSE, Quality Rep, Planner, etc.) with <em>+ Add Role</em>.</li>
|
||
<li><strong>Work Package Types</strong> — enable the install types this project uses (Conduit Install, Wire Pull, Terminations, …). Enabled types populate the WP type picker.</li>
|
||
<li><strong>Governance & WP Numbering</strong> — the WP <strong>number format</strong> (e.g. <code>WP##-[Sector]-[TYPE]</code>), issuance strategy, the project's <strong>disciplines</strong>, the <strong>discipline strategy</strong>, and <strong>WP sizing</strong> (see <a data-help-jump="sizing">Sizing</a> and <a data-help-jump="disciplines">Disciplines</a>).</li>
|
||
<li><strong>Quality & Inspection Strategy</strong> — QC requirement, photo/documentation standard, and hold/witness points.</li>
|
||
<li><strong>Tracking & Commissioning Platforms</strong> — e.g. CxAlloy, Procore, ACC.</li>
|
||
<li><strong>Construction Sequence</strong> — the install flow; reorder by dragging (⠿), edit labels, add <em>◆ QC Hold</em> gates or custom steps. These feed the WP "predecessor" picker.</li>
|
||
<li><strong>Release Gate Constraints</strong> — choose which standard AWP constraints apply and add custom ones (see <a data-help-jump="constraints">Constraints</a>).</li>
|
||
<li><strong>Engineering Sources & References</strong> — labelled links (Design Drawings, Specs, …) that appear as quick-access buttons in the WP Creator's <em>Drawings & Attachments</em>.</li>
|
||
</ol>
|
||
<div class="ui-help-callout">Fields a WP inherits from the SOP show a <strong>"from SOP"</strong> tag and are locked. You can override a locked field with <strong>🔒 Edit</strong>, which requires a logged reason.</div>` },
|
||
|
||
{ id: 'wps', title: 'Work Packages', body: `
|
||
<h3>Creating Work Packages</h3>
|
||
<p>In the Creator, start a package with <strong>+ New</strong> (blank, auto-numbered) or <strong>⧉ Duplicate</strong> (copies a saved package and increments the number). The <strong>WP Number</strong> is built automatically from the SOP number format plus your scope fields, the WP type, and a counter — it's read-only.</p>
|
||
<h4>Key fields</h4>
|
||
<ul>
|
||
<li><strong>Subject / Title</strong> (required) and <strong>WP Type</strong> (required, from the SOP).</li>
|
||
<li><strong>Assets</strong> — link each controls.dev asset the package covers.</li>
|
||
<li><strong>Disciplines</strong> — which trades the package covers (see <a data-help-jump="disciplines">Disciplines & Split</a>).</li>
|
||
<li><strong>Scope & Work</strong> — the sequenced steps the crew performs (per-discipline in multi-discipline mode).</li>
|
||
<li><strong>Labor – Est. Hrs.</strong> — drives the sizing check (see <a data-help-jump="sizing">Sizing</a>).</li>
|
||
<li><strong>Material List</strong> — the bill of materials; import from CSV/Excel or add lines manually.</li>
|
||
<li><strong>Drawings & Attachments</strong> — documents and SOP source-folder links.</li>
|
||
<li><strong>Kitting & Material Movement (MIMO)</strong> — kitting status, warehouse owner, move date/location.</li>
|
||
<li><strong>Constraints</strong> — the release gate (see <a data-help-jump="constraints">Constraints</a>).</li>
|
||
<li><strong>Quality / Hold Points</strong>, <strong>Approvals & Sign-offs</strong>, and <strong>Closeout</strong> (actual hours, as-builts, lessons learned — shown at QC/Closed).</li>
|
||
</ul>
|
||
<h4>Saving</h4>
|
||
<p><strong>Save Draft</strong> stores the package; <strong>⚡ Save & View</strong> saves and renders the print-ready output. Drafts auto-save to your browser as you type, so nothing is lost if you close the tab.</p>` },
|
||
|
||
{ id: 'statuses', title: 'Statuses', body: `
|
||
<h3>Work Package statuses</h3>
|
||
<table>
|
||
<tr><th>Status</th><th>Meaning</th></tr>
|
||
<tr><td><span class="ui-help-pill pill-draft">Draft</span></td><td>Work in progress; not yet released.</td></tr>
|
||
<tr><td><span class="ui-help-pill pill-sched">Scheduled</span></td><td>Planned and scheduled; upcoming.</td></tr>
|
||
<tr><td><span class="ui-help-pill pill-issued">Issued</span></td><td>Released to the field. Requires <em>all constraints Cleared or N/A</em>.</td></tr>
|
||
<tr><td><span class="ui-help-pill pill-prog">In Progress</span></td><td>Actively being worked.</td></tr>
|
||
<tr><td><span class="ui-help-pill pill-qc">QC</span></td><td>In quality check / inspection.</td></tr>
|
||
<tr><td><span class="ui-help-pill pill-closed">Closed</span></td><td>Completed.</td></tr>
|
||
<tr><td><span class="ui-help-pill pill-hold">Issue (Hold)</span></td><td>A constraint reopened after release — work is paused until it's resolved.</td></tr>
|
||
</table>
|
||
<div class="ui-help-callout">A package <strong>cannot move to Issued</strong> while any constraint is Open. If a constraint reopens after a package is Issued, its status automatically drops to <strong>Issue (Hold)</strong> and the suite makes you log what happened.</div>
|
||
<p>On a multi-discipline package, each discipline carries its own status and the overall status <strong>rolls up to the least-advanced discipline</strong> — so a package is never "Closed" while one trade still lags.</p>` },
|
||
|
||
{ id: 'constraints', title: 'Constraints & release', body: `
|
||
<h3>Constraints & release readiness</h3>
|
||
<p>Constraints are the readiness checklist that gates a package's release to the field. They follow Advanced Work Packaging (AWP Vol II §2.3.2). The standard set:</p>
|
||
<ol>
|
||
<li>Safety & Permitting</li><li>Quality Control / Inspection</li><li>IFC Drawings & Specs</li>
|
||
<li>Schedule</li><li>Materials (on site, bagged & tagged)</li><li>Prefabrication</li>
|
||
<li>Work Access & Laydown</li><li>Craft Availability</li><li>Construction Equipment & Tools</li>
|
||
<li>Scaffolding / Access Equipment</li>
|
||
</ol>
|
||
<p>Pick which apply (and add custom ones) in <strong>SOP Step 9</strong>. Each constraint on a package has one of three states:</p>
|
||
<table>
|
||
<tr><th>State</th><th>Effect</th></tr>
|
||
<tr><td><strong>Open</strong></td><td>Not yet cleared — <em>blocks release</em>.</td></tr>
|
||
<tr><td><strong>Cleared</strong></td><td>Requirement met — counts toward release-ready.</td></tr>
|
||
<tr><td><strong>N/A</strong></td><td>Not applicable to this package — counts as cleared.</td></tr>
|
||
</table>
|
||
<h4>The release gate</h4>
|
||
<ul>
|
||
<li>A package is <strong>release-ready</strong> when every constraint is Cleared or N/A. The sticky banner shows green when ready, amber when constraints are still open, and red when on hold.</li>
|
||
<li>When the last open constraint clears, the suite offers to mark the package <strong>Issued</strong>.</li>
|
||
<li>If a constraint reopens after the package is Issued, you log the hold (what reopened, details, optional doc link & photo) and the status drops to <strong>Issue (Hold)</strong>.</li>
|
||
</ul>` },
|
||
|
||
{ id: 'disciplines', title: 'Disciplines & Split', body: `
|
||
<h3>Disciplines & Split by Discipline</h3>
|
||
<p>Disciplines are trades (Mechanical, Electrical, Tech, …) set in <strong>SOP Step 5</strong>. The <strong>discipline strategy</strong> controls how packages handle them:</p>
|
||
<ul>
|
||
<li><strong>Let the planner choose per package</strong> (recommended) — pick one discipline (flat scope) or several (per-discipline scope + the <em>Split</em> option).</li>
|
||
<li><strong>One discipline per package</strong> — each WP is single-discipline.</li>
|
||
<li><strong>Multiple disciplines per package</strong> — scope is always split by discipline.</li>
|
||
</ul>
|
||
<h4>Split by Discipline</h4>
|
||
<p>When a package covers 2+ disciplines, the <strong>⎘ Split by Discipline</strong> button breaks it into one numbered instance per discipline — <code>WP01A</code>, <code>WP01B</code>, <code>WP01C</code> (or <code>_MECH</code>/<code>_ELEC</code> suffixes, set in the SOP). The original is kept as a <strong>master / roll-up</strong>; each instance:</p>
|
||
<ul>
|
||
<li>becomes its own single-discipline package, issued independently;</li>
|
||
<li>receives only the <strong>scope steps</strong> and <strong>materials tagged to that discipline</strong>;</li>
|
||
<li>stays linked back to the master.</li>
|
||
</ul>
|
||
<div class="ui-help-callout">Tag material rows to a discipline <em>before</em> splitting. <strong>Untagged rows stay on the master only</strong> and won't be routed to any instance. Masters are excluded from dashboard counts so hours aren't double-counted.</div>` },
|
||
|
||
{ id: 'sizing', title: 'Sizing', body: `
|
||
<h3>Work Package sizing</h3>
|
||
<p>In <strong>SOP Step 5</strong> you set a typical WP <strong>size band</strong>, which sets a <strong>split threshold</strong> (max labor hours):</p>
|
||
<table>
|
||
<tr><th>Size band</th><th>Split threshold</th></tr>
|
||
<tr><td>Small — 1–2 days (≈8–24 hrs)</td><td>24 hrs</td></tr>
|
||
<tr><td>Standard — 3–5 days (≈40–80 hrs)</td><td>80 hrs</td></tr>
|
||
<tr><td>Large — 1–2 weeks (≈80–160 hrs)</td><td>160 hrs</td></tr>
|
||
<tr><td>Custom…</td><td>you set it</td></tr>
|
||
</table>
|
||
<p>In the Creator, the <strong>Est. Hrs.</strong> field is checked live against the threshold. Within range you see the target band; over it you get an amber warning — <em>"⚠ … exceeds the …-hr split threshold — consider breaking this package down"</em> — and a nudge to split by discipline where that applies. It's a guide, not a hard block: you can proceed if it's intentional.</p>` },
|
||
|
||
{ id: 'dashboard', title: 'Dashboard', body: `
|
||
<h3>Dashboard & metrics</h3>
|
||
<p>The dashboard aggregates every (non-master) package in the active project. Open it from the home page, the suite's <strong>📊 Dashboard</strong> tab, or the Creator header.</p>
|
||
<h4>Metric cards (click to filter)</h4>
|
||
<ul>
|
||
<li><strong>Total WPs</strong>, <strong>Release-ready</strong>, <strong>On hold</strong>, <strong>Overdue</strong></li>
|
||
<li><strong>Est. hrs</strong> and <strong>Actual hrs</strong> (summed)</li>
|
||
</ul>
|
||
<h4>Breakdowns & gates</h4>
|
||
<ul>
|
||
<li><strong>By status</strong> and <strong>by discipline</strong> chips.</li>
|
||
<li><strong>⛔ Gating constraints</strong> — lists every blocked package and exactly which constraints are holding it.</li>
|
||
</ul>
|
||
<h4>The table</h4>
|
||
<p>Shows WP #, subject, type, discipline, status, <strong>Gates</strong> (<em>clear</em>, <em>n open</em>, or <em>master</em>), due date (red if overdue), and hours. Row actions: <strong>issue</strong> (when release-ready), <strong>view</strong>, and <strong>edit</strong>. Filter with the search box and the status / discipline dropdowns.</p>
|
||
<div class="ui-help-callout">Split <strong>masters</strong> are labelled and excluded from the counts; you issue their instances one at a time as each becomes release-ready.</div>` },
|
||
|
||
{ id: 'data', title: 'Samples, sharing & comments', body: `
|
||
<h3>Samples, import / export & comments</h3>
|
||
<h4>Load Sample</h4>
|
||
<p><strong>⭐ Load Sample</strong> is context-aware: on the SOP tab it loads a complete sample SOP; on the WP tab it loads an example Work Package. Great for learning the tool or demoing.</p>
|
||
<h4>Import / Export</h4>
|
||
<ul>
|
||
<li><strong>Work Packages</strong> — <em>⤓ Export (JSON)</em> downloads all saved packages; import restores them.</li>
|
||
<li><strong>SOP</strong> — the Creator can import a SOP <code>.json</code> (via <em>⤒ Import SOP</em>) or load the sample SOP.</li>
|
||
<li><strong>Materials</strong> — import a bill of materials from Excel/CSV, or download a template.</li>
|
||
</ul>
|
||
<h4>Comments & feedback</h4>
|
||
<p>Leave feedback from the home page, per-step comments in the SOP tool (<strong>💬 Step Comments</strong>), or package comments in the Creator's <strong>💬 Comments</strong> drawer. Comments are saved and can be exported/imported as <code>.json</code> so reviewers can share them — and, when the API is reachable, they're collected centrally too.</p>
|
||
<h4>Usage logs</h4>
|
||
<p><strong>📊 Usage Logs</strong> / <strong>▤ Usage Data</strong> shows session and event counts and can export the full log. A <strong>dev-mode</strong> toggle pauses tracking during demos.</p>` },
|
||
|
||
{ id: 'shortcuts', title: 'Tips & shortcuts', body: `
|
||
<h3>Tips & keyboard shortcuts</h3>
|
||
<ul>
|
||
<li><strong>Enter</strong> in a sequence, constraint, or material input adds/saves that row.</li>
|
||
<li><strong>Esc</strong> closes any modal — this help center, comments, the constraint library, and the hold-log dialog.</li>
|
||
<li>Hover any <span class="help-tip" data-tip="Like this one — hover any ⓘ for a hint.">i</span> icon for an inline hint.</li>
|
||
<li>Data is kept <strong>per project</strong> — switch projects from the home page.</li>
|
||
<li>Your work <strong>auto-saves</strong> to the browser as you type; <em>Save & View</em> produces the print-ready output.</li>
|
||
<li>Click a metric card or status chip on the <strong>Dashboard</strong> to filter the table.</li>
|
||
</ul>` },
|
||
|
||
{ id: 'glossary', title: 'Glossary', body: `
|
||
<h3>Glossary</h3>
|
||
<table>
|
||
<tr><th>Term</th><th>Meaning</th></tr>
|
||
<tr><td><strong>IWP</strong></td><td>Installation Work Package — the field-level package this tool produces.</td></tr>
|
||
<tr><td><strong>AWP</strong></td><td>Advanced Work Packaging — the methodology behind the constraint set and release gate.</td></tr>
|
||
<tr><td><strong>SOP</strong></td><td>Standard Operating Procedure — the project baseline every WP inherits.</td></tr>
|
||
<tr><td><strong>Constraint</strong></td><td>A readiness item (Open / Cleared / N/A) that gates release.</td></tr>
|
||
<tr><td><strong>Release-ready</strong></td><td>All constraints Cleared or N/A — the package can be Issued.</td></tr>
|
||
<tr><td><strong>Issued</strong></td><td>Released to the field.</td></tr>
|
||
<tr><td><strong>Issue (Hold)</strong></td><td>A released package paused because a constraint reopened.</td></tr>
|
||
<tr><td><strong>Discipline</strong></td><td>A trade (Mechanical, Electrical, Tech, …).</td></tr>
|
||
<tr><td><strong>Split / Master / Instance</strong></td><td>Breaking a multi-discipline package (master/roll-up) into single-discipline instances (WP01A/B/C).</td></tr>
|
||
<tr><td><strong>Scope</strong></td><td>The sequenced steps the crew performs.</td></tr>
|
||
<tr><td><strong>Sequence</strong></td><td>SOP-defined construction phases; a WP can name a predecessor step.</td></tr>
|
||
<tr><td><strong>Bagged & tagged</strong></td><td>Materials on site, kitted, and labelled — part of the Materials constraint.</td></tr>
|
||
<tr><td><strong>MIMO</strong></td><td>Material In / Material Out — kitting and staging logistics.</td></tr>
|
||
<tr><td><strong>Asset</strong></td><td>A controls.dev record (equipment/system) a package is built around.</td></tr>
|
||
<tr><td><strong>Hold / Witness point</strong></td><td>Hold = work stops until inspection sign-off; Witness = inspection offered but work may proceed.</td></tr>
|
||
<tr><td><strong>Active project</strong></td><td>The currently selected project; all data is scoped to it.</td></tr>
|
||
</table>` },
|
||
|
||
{ id: 'faq', title: 'FAQ', body: `
|
||
<h3>Frequently asked questions</h3>
|
||
<h4>The Work Package Creator is locked — why?</h4>
|
||
<p>The SOP for the active project isn't complete yet. Finish SOP Configuration and click <strong>✓ SOP Complete</strong> on the last step; the Creator unlocks and the home card turns green.</p>
|
||
<h4>Why can't I set a package to Issued?</h4>
|
||
<p>At least one constraint is still <strong>Open</strong>. Clear or mark N/A every constraint — the release banner turns green — and the suite will offer to issue it.</p>
|
||
<h4>My package's materials didn't all carry over when I split it.</h4>
|
||
<p>Only material rows <strong>tagged to a discipline</strong> are routed to that instance. Untagged rows stay on the master. Tag them before splitting.</p>
|
||
<h4>Why don't split masters show in the dashboard totals?</h4>
|
||
<p>Masters are roll-ups; counting them would double-count their hours and packages. The individual instances are counted instead.</p>
|
||
<h4>Will I lose my work if I close the browser?</h4>
|
||
<p>No — drafts auto-save locally per project and reload next time. Use <em>Export (JSON)</em> for a backup or to share with a teammate.</p>
|
||
<h4>Does each project keep its own data?</h4>
|
||
<p>Yes. SOP and Work Packages are scoped to the active project; switching projects loads that project's own set.</p>
|
||
<h4>How do I report a problem or suggestion?</h4>
|
||
<p>Use the feedback / comments features (home page, SOP <em>Step Comments</em>, or the Creator's <em>Comments</em> drawer).</p>` }
|
||
];
|
||
|
||
// ── build ─────────────────────────────────────────────────────────────────
|
||
function buildModal() {
|
||
if (document.getElementById('ui-help-overlay')) return;
|
||
|
||
var overlay = document.createElement('div');
|
||
overlay.className = 'ui-help-overlay';
|
||
overlay.id = 'ui-help-overlay';
|
||
|
||
var nav = TOPICS.map(function (t) {
|
||
return '<a data-help-target="' + t.id + '">' + t.title + '</a>';
|
||
}).join('');
|
||
|
||
var sections = TOPICS.map(function (t) {
|
||
return '<section class="ui-help-sec" id="ui-help-sec-' + t.id + '">' + t.body + '</section>';
|
||
}).join('');
|
||
|
||
overlay.innerHTML =
|
||
'<div class="ui-help-modal" role="dialog" aria-modal="true" aria-label="Help center">' +
|
||
'<div class="ui-help-head">' +
|
||
'<span class="ui-help-title">Help — Work Package Suite</span>' +
|
||
'<span class="ui-help-search"><input id="ui-help-q" type="search" placeholder="Search help…" aria-label="Search help"></span>' +
|
||
'<button type="button" class="ui-help-x" onclick="closeHelp()" aria-label="Close help">✕</button>' +
|
||
'</div>' +
|
||
'<div class="ui-help-wrap">' +
|
||
'<nav class="ui-help-nav" id="ui-help-nav">' + nav + '</nav>' +
|
||
'<div class="ui-help-content" id="ui-help-content">' +
|
||
'<p class="ui-help-noresult" id="ui-help-noresult">No matches. Try another word.</p>' +
|
||
sections +
|
||
'</div>' +
|
||
'</div>' +
|
||
'</div>';
|
||
|
||
overlay.addEventListener('click', function (e) { if (e.target === overlay) closeHelp(); });
|
||
document.body.appendChild(overlay);
|
||
|
||
// nav clicks + in-content cross-links jump to a section
|
||
overlay.addEventListener('click', function (e) {
|
||
var t = e.target.closest('[data-help-target],[data-help-jump]');
|
||
if (!t) return;
|
||
e.preventDefault();
|
||
jumpTo(t.getAttribute('data-help-target') || t.getAttribute('data-help-jump'));
|
||
});
|
||
|
||
// search
|
||
var q = overlay.querySelector('#ui-help-q');
|
||
q.addEventListener('input', function () { runSearch(q.value); });
|
||
|
||
// highlight nav as you scroll
|
||
var content = overlay.querySelector('#ui-help-content');
|
||
content.addEventListener('scroll', syncActiveNav, { passive: true });
|
||
}
|
||
|
||
function jumpTo(id) {
|
||
var sec = document.getElementById('ui-help-sec-' + id);
|
||
if (!sec) return;
|
||
// Clear any active search filter so the target is visible.
|
||
var q = document.getElementById('ui-help-q');
|
||
if (q && q.value) { q.value = ''; runSearch(''); }
|
||
sec.scrollIntoView({ block: 'start' });
|
||
setActiveNav(id);
|
||
}
|
||
|
||
function setActiveNav(id) {
|
||
var nav = document.getElementById('ui-help-nav');
|
||
if (!nav) return;
|
||
nav.querySelectorAll('a').forEach(function (a) {
|
||
a.classList.toggle('active', a.getAttribute('data-help-target') === id);
|
||
});
|
||
}
|
||
|
||
function syncActiveNav() {
|
||
var content = document.getElementById('ui-help-content');
|
||
if (!content) return;
|
||
var top = content.scrollTop, best = null, bestDist = Infinity;
|
||
TOPICS.forEach(function (t) {
|
||
var sec = document.getElementById('ui-help-sec-' + t.id);
|
||
if (!sec || sec.classList.contains('hide')) return;
|
||
var d = Math.abs(sec.offsetTop - top);
|
||
if (sec.offsetTop - top <= 40 && d < bestDist) { bestDist = d; best = t.id; }
|
||
});
|
||
if (best) setActiveNav(best);
|
||
}
|
||
|
||
// ── search: filter sections + highlight matches ───────────────────────────
|
||
function clearMarks(root) {
|
||
root.querySelectorAll('mark').forEach(function (m) {
|
||
var txt = document.createTextNode(m.textContent);
|
||
m.parentNode.replaceChild(txt, m);
|
||
});
|
||
root.normalize();
|
||
}
|
||
|
||
function markMatches(el, query) {
|
||
var lower = query.toLowerCase();
|
||
var walker = document.createTreeWalker(el, NodeFilter.SHOW_TEXT, {
|
||
acceptNode: function (node) {
|
||
if (!node.nodeValue.trim()) return NodeFilter.FILTER_REJECT;
|
||
var p = node.parentNode.nodeName;
|
||
if (p === 'MARK' || p === 'STYLE' || p === 'SCRIPT') return NodeFilter.FILTER_REJECT;
|
||
return node.nodeValue.toLowerCase().indexOf(lower) >= 0 ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_REJECT;
|
||
}
|
||
});
|
||
var nodes = [], n;
|
||
while ((n = walker.nextNode())) nodes.push(n);
|
||
nodes.forEach(function (node) {
|
||
var val = node.nodeValue, low = val.toLowerCase(), frag = document.createDocumentFragment(), i = 0, idx;
|
||
while ((idx = low.indexOf(lower, i)) >= 0) {
|
||
if (idx > i) frag.appendChild(document.createTextNode(val.slice(i, idx)));
|
||
var mk = document.createElement('mark');
|
||
mk.textContent = val.slice(idx, idx + query.length);
|
||
frag.appendChild(mk);
|
||
i = idx + query.length;
|
||
}
|
||
if (i < val.length) frag.appendChild(document.createTextNode(val.slice(i)));
|
||
node.parentNode.replaceChild(frag, node);
|
||
});
|
||
}
|
||
|
||
function runSearch(query) {
|
||
var content = document.getElementById('ui-help-content');
|
||
var nav = document.getElementById('ui-help-nav');
|
||
var noresult = document.getElementById('ui-help-noresult');
|
||
if (!content) return;
|
||
query = (query || '').trim();
|
||
var hits = 0;
|
||
|
||
TOPICS.forEach(function (t) {
|
||
var sec = document.getElementById('ui-help-sec-' + t.id);
|
||
var navItem = nav.querySelector('[data-help-target="' + t.id + '"]');
|
||
clearMarks(sec);
|
||
var match = !query || sec.textContent.toLowerCase().indexOf(query.toLowerCase()) >= 0;
|
||
sec.classList.toggle('hide', !match);
|
||
if (navItem) navItem.classList.toggle('nohit', !!query && !match);
|
||
if (match) {
|
||
hits++;
|
||
if (query) markMatches(sec, query);
|
||
}
|
||
});
|
||
|
||
noresult.style.display = (query && hits === 0) ? 'block' : 'none';
|
||
if (query) { content.scrollTop = 0; }
|
||
else { syncActiveNav(); }
|
||
}
|
||
|
||
// ── public API ──────────────────────────────────────────────────────────────
|
||
global.openHelp = function (topicId) {
|
||
buildModal();
|
||
document.getElementById('ui-help-overlay').classList.add('open');
|
||
var q = document.getElementById('ui-help-q');
|
||
if (topicId && typeof topicId === 'string') jumpTo(topicId);
|
||
else { setActiveNav(TOPICS[0].id); if (q) setTimeout(function () { q.focus(); }, 30); }
|
||
};
|
||
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();
|
||
});
|
||
|
||
// ── floating launcher on pages without their own Help button ────────────────
|
||
function maybeAddFab() {
|
||
var inIframe = (function () { try { return window.top !== window.self; } catch (e) { return true; } })();
|
||
if (inIframe || global.WP_HELP_NO_FAB) return; // suite shows the parent's button
|
||
if (document.querySelector('[onclick*="openHelp"]')) return; // page already has a Help trigger
|
||
if (document.getElementById('ui-help-fab')) return;
|
||
var b = document.createElement('button');
|
||
b.id = 'ui-help-fab'; b.className = 'ui-help-fab'; b.type = 'button';
|
||
b.title = 'Help'; b.setAttribute('aria-label', 'Open help'); b.textContent = '?';
|
||
b.addEventListener('click', function () { global.openHelp(); });
|
||
document.body.appendChild(b);
|
||
}
|
||
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', maybeAddFab);
|
||
else maybeAddFab();
|
||
})(window);
|