Files
Project-SDE-WP-Suite/html/wp-creation-index.html
n.siegfried 4d186ea6a0 T6.3/T6.4 - CR-004 and CR-018: picked not typed, and totals that add up
CR-004 and CR-018 are the same change seen from two ends. CR-018 is why the
Acumatica cost code came out rather than being relabelled — the tracking
dimension the team wants is floor and area, not an accounting code — and CR-004
is what makes that dimension exist. Committed together because a rollup keyed on
free text is not a rollup, and structured location with nothing rolling up by it
is a form change nobody asked for.

CR-004 - three dependent dropdowns

  Building filters Floor filters Sector, off the project's own taxonomy from
  T5.4. Clearing a parent clears its children: not doing that is how a package
  ends up filed under a floor that is not in the building it claims.

  PATHS are stored, not names and not bare codes. A floor's own code is not
  unique across buildings; `B-ONE/L1` is. That is what lets the dashboard filter
  by a building and match everything beneath it with a prefix test, and it is
  what CR-018 groups on.

  The list is fetched with include_inactive=true, which is not a contradiction of
  CR-005's "deactivating hides it from new work packages" — they are two
  questions. What may be CHOSEN is active only. What may be SHOWN is everything,
  because a package already referencing a deactivated value still has to render
  its label, and blanking it on open would write the blank back on the next save.
  A deactivated value that IS on the package is offered, labelled "(no longer
  offered)"; on a fresh package it is not offered at all. Both checked.

  X5, checked the way aggregates_check checks its own: localStorage is poisoned
  with a fake building and the dropdown is required to ignore it.

  wp_location survives as a hidden field. A package written before this keeps
  what it said, and the form says so rather than dropping it.

CR-018 - the rollup

  LOCATION_DIMENSIONS is now ("building", "floor", "sector"). T4.1's note said
  "only this tuple and the keys inside each group change - the response shape
  does not", and that held exactly.

  Rolled up at EVERY level, server-side, not just at the leaf. "How many on
  floor 2" is the question CR-018 asks and it is a level above the leaf groups;
  summing them in the browser would be the same per-browser arithmetic B4
  removed. Actual Hours rolls up along the same dimensions - that is the field
  CR-017 retained, and this is why that decision mattered.

  Packages with no location are an explicit "(unassigned)" row, not a gap. The
  reason is arithmetic: a group set that silently omits them does not add up to
  the project total, and a rollup that does not reconcile is decoration. The
  probe checks every level sums to the project total, and to the estimated and
  actual hour totals, using distinct primes so a mis-sum cannot land on the
  right number by luck.

  A package with a building but no floor lands in the floor-level unassigned row
  alongside the one with no location at all - which is the honest answer, and is
  asserted by its hours rather than by its count.

  Free text captured before CR-004 groups under itself as a building rather than
  collapsing into unassigned, one level deep. Pretending free text is a
  hierarchy would file "FAB / LVL 1" under a building called "FAB / LVL 1".

  server/app.py                 dimensions, _location_levels, hours per group
  html/wp-creation-index.html   three selects where the text box was
  html/wp-creation-app.js       the pickers, the filters, the rollup panel
  html/wp-creation-styles.css   .loc-picker, .loc-rollup
  tests/rollup_check.py         new - 63 checks

Done when — CR-004
  [x] all three render as dropdowns populated from project configuration
  [x] dependent filtering works, and clearing a parent clears its children
  [x] values persist as codes; confirmed by reading what collectPackage stored
  [x] the dashboard filters by each of the three
  [x] a work package referencing a deactivated value still renders correctly
  [x] all option data comes from the server - proved by poisoning the cache

Done when — CR-018
  [x] the dashboard groups and totals by Building, Floor and Sector
  [x] totals reconcile against an unfiltered count, at every level
  [x] Actual Hours rolls up along the same dimensions
  [x] grouping is computed server-side - proved by putting nine fake packages in
      localStorage and requiring the panel to show none of them
  [x] work packages with no location appear in an explicit unassigned group

No migration: location lives in the work package's JSON data blob like every
other per-package field. No colour literal added.

Verified one at a time
  rollup_check      63/63  new
  generalinfo_check 49/49
  browser_check     71/71
  pipeline          43/43
  a11y              22/22
  aggregates        16/16
  f_items           F1-F5 FIXED, F6 REPRODUCES (T7.2)

Question for the PR, per CLAUDE.md: the dashboard's location filters and the
rollup both key on the path, so a package saved with free text and no codes is
unreachable by any location filter and sits in its own building-level row. That
is correct and it is also a migration question - whether the existing free-text
locations should be mapped onto the taxonomy once the B100 list arrives, or left
as history. Nothing here decides it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-17 00:24:00 -05:00

459 lines
34 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Work Package (IWP) — Prime Controls</title>
<script src="auth-guard.js"></script>
<!-- Date/number formatting. Must parse BEFORE the app scripts: they format
timestamps during their own boot. -->
<script src="wp-format.js"></script>
<!-- Addressable state (S3). Parses before the app scripts, which read the URL
during their own boot. -->
<script src="wp-url.js"></script>
<!-- Autosave, unsaved-work guard, draft recovery (S2). -->
<script src="wp-autosave.js"></script>
<!-- Which sections this project uses (CR-006). The same file the SOP wizard
reads, so "Assets is off" means one thing in both. -->
<script src="wp-sections.js"></script>
<link rel="icon" href="favicon.ico" sizes="any">
<link rel="manifest" href="manifest.webmanifest">
<meta name="theme-color" content="#161616">
<link rel="stylesheet" href="theme-light.css">
<link rel="stylesheet" href="wp-creation-styles.css">
<link rel="stylesheet" href="wp-sidenav.css">
</head>
<body>
<div class="loading-overlay" id="loadingOverlay"><div class="spinner"></div><div class="loading-text">Saving work package…</div></div>
<div class="header">
<!-- The logo chip, not the mono wordmark this page used to carry: every other page
in the suite shows the chip, and the creator was the only one spelling the brand
out in monospace (T2.2). -->
<div class="logo-wrap embed-hide">
<a href="index.html" class="header-logo-chip" title="Work Package Suite home">
<span class="wp-logo-chip"><img src="prime-controls-logo.jpg" alt="Prime Controls"></span>
</a>
<button id="dev-toggle" class="dev-toggle" onclick="toggleDevMode()" title="dev mode" aria-label="dev mode"></button>
</div>
<div class="header-sep embed-hide">|</div>
<div class="header-title">Work Package (IWP)</div>
<button class="btn btn-ghost embed-hide" style="margin-left:auto;padding:7px 16px" onclick="document.getElementById('sop-import').click()">⤒ Import SOP</button>
<input type="file" id="sop-import" accept="application/json" style="display:none" onchange="importSOP(event)">
<button class="btn btn-ghost embed-hide" style="padding:7px 16px" onclick="loadSampleSOP()">⤓ Sample SOP</button>
<button class="btn btn-ghost embed-hide" style="padding:7px 16px" onclick="openSopModal()">👁 View SOP</button>
<button class="btn btn-ghost embed-hide" style="padding:7px 16px" onclick="loadExample()">★ Load example</button>
<button class="btn btn-ghost embed-hide" style="padding:7px 16px" onclick="showDashboard()">📊 Dashboard</button>
<button class="btn btn-ghost embed-first" style="padding:7px 16px" onclick="newPackage()">+ New</button>
<button class="btn btn-ghost" style="padding:7px 16px" onclick="duplicateWP()">⧉ Duplicate</button>
<button class="btn btn-ghost" style="padding:7px 16px" onclick="showHistoryCurrent()" title="Change history for this work package">🕘 History</button>
<button class="btn btn-ghost embed-hide" id="comments-btn" style="padding:7px 16px" onclick="toggleComments()">💬 Comments <span class="cbadge-total" id="cbadge-total" style="display:none">0</span></button>
<button class="btn btn-ghost embed-hide" style="padding:7px 16px" onclick="showAnalytics()">▤ Usage data</button>
</div>
<div class="dev-banner" id="dev-banner" style="display:none">⚙ DEV MODE — usage tracking paused. This session's actions are not being recorded.</div>
<div class="ctx-bar" id="ctx-bar"></div>
<!-- RELEASE READINESS BANNER -->
<div class="release-banner" id="release-banner"></div>
<!-- SECTION NAV (jump links, built from the form cards) -->
<div class="section-nav-bar" id="section-nav"></div>
<div class="wp-layout">
<!-- WORK PACKAGE NAVIGATOR
A persistent side panel (not a hover drawer): collapse toggle, a primary action,
icon nav, then the project's packages as rows with colour-coded initial badges.
Collapsing leaves a narrow icon rail so you can still see and switch packages. -->
<aside class="wp-nav" id="wp-nav" aria-label="Work packages">
<div class="wp-nav-top">
<button class="wp-nav-toggle" id="wp-nav-toggle" onclick="toggleWpNav()"
title="Collapse the panel" aria-label="Collapse the panel" aria-expanded="true">
<svg viewBox="0 0 20 20" width="18" height="18" aria-hidden="true">
<rect x="2.5" y="3.5" width="15" height="13" rx="1.5" fill="none" stroke="currentColor" stroke-width="1.4"/>
<line x1="7.5" y1="3.5" x2="7.5" y2="16.5" stroke="currentColor" stroke-width="1.4"/>
<path class="wp-nav-toggle-arrow" d="M14 10 H10 M11.6 8.2 L9.8 10 L11.6 11.8"
fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round"/>
</svg>
</button>
</div>
<div class="wp-nav-primary">
<button class="wp-nav-cta" onclick="newPackage()" title="Start a new work package">
<span class="wp-nav-cta-plus" aria-hidden="true">+</span><span class="wp-nav-cta-label">New work package</span>
</button>
<button class="wp-nav-cta-more" id="wp-nav-more-btn" onclick="toggleWpNavMore(event)"
title="More actions" aria-label="More actions" aria-haspopup="true" aria-expanded="false"></button>
<div class="wp-nav-menu" id="wp-nav-more" hidden>
<button type="button" onclick="wpNavAction('duplicate')">Duplicate this package</button>
<button type="button" onclick="wpNavAction('split')">Split by discipline</button>
<button type="button" onclick="wpNavAction('export')">Export all (JSON)</button>
</div>
</div>
<nav class="wp-nav-links" aria-label="Views">
<button type="button" class="wp-nav-link" data-view="mine" onclick="setWpNavView('mine')" title="Packages you own">
<span class="wp-nav-ico" aria-hidden="true"></span><span class="wp-nav-link-label">My packages</span>
<span class="wp-nav-link-n" id="wp-nav-n-mine"></span>
</button>
<button type="button" class="wp-nav-link is-current" data-view="all" onclick="setWpNavView('all')" title="Every package on this project">
<span class="wp-nav-ico" aria-hidden="true"></span><span class="wp-nav-link-label">All packages</span>
<span class="wp-nav-link-n" id="wp-nav-n-all"></span>
</button>
<button type="button" class="wp-nav-link" data-view="open" onclick="setWpNavView('open')" title="Not release-ready yet">
<span class="wp-nav-ico" aria-hidden="true"></span><span class="wp-nav-link-label">Needs attention</span>
<span class="wp-nav-link-n" id="wp-nav-n-open"></span>
</button>
<button type="button" class="wp-nav-link" onclick="showDashboard()" title="Status and gating across the project">
<span class="wp-nav-ico" aria-hidden="true"></span><span class="wp-nav-link-label">Dashboard</span>
</button>
</nav>
<div class="wp-nav-sect">
<span class="wp-nav-sect-label" id="wp-nav-sect-label">Work packages</span>
<span class="wp-nav-count" id="wp-nav-count"></span>
</div>
<div class="wp-nav-filter">
<input type="search" class="wp-nav-search" id="wp-nav-search" placeholder="Filter packages…" oninput="renderWpNav()">
</div>
<div class="wp-nav-list" id="wp-nav-list"></div>
</aside>
<div class="main">
<!-- PACKAGE KIND (only shown when the project's SOP includes BIM/VDC) -->
<div class="card" id="kind-row" style="display:none">
<div class="sub-heading">Package Type</div>
<div class="notice">This project includes BIM/VDC packages. Choose what this one is — it tailors the fields below and the WP types / release gates offered.</div>
<div class="radio-group" id="kind-group" style="margin-bottom:0">
<label class="radio-pill" data-val="iwp"><input type="radio" name="pkgkind" onclick="setKind('iwp')"><span class="dot"></span>Install package (IWP)</label>
<label class="radio-pill" data-val="ewp"><input type="radio" name="pkgkind" onclick="setKind('ewp')"><span class="dot"></span>BIM package (EWP)</label>
</div>
</div>
<!-- GENERAL INFORMATION -->
<!-- CR-006 gives every toggleable section a stable id. The map lives in
wp-creation-app.js (WP_SECTION_NODES); the ids are what it addresses. -->
<div class="card" id="general-card">
<div class="section-header"><div class="section-title">General Information</div>
<div class="section-desc">Parameters in <span style="color:var(--accent)">blue</span> are inherited from the project SOP. Fill the rest for this package.</div></div>
<div class="field-grid">
<div class="field"><label>WP number <span class="auto-tag">auto</span><span class="help-tip" data-tip="Built automatically from the SOP number format — the scope fields below (e.g. Sector) plus the WP type and a sequence counter.">i</span></label><input type="text" id="wp_number" readonly class="locked-field" placeholder="auto-built"><div class="field-hint sop-hint" id="wp_number_hint"></div></div>
<div class="field"><label>Status</label>
<div class="radio-group" id="status-group" style="margin-bottom:0">
<label class="radio-pill" data-val="Draft"><input type="radio" name="status"><span class="dot"></span>Draft</label>
<label class="radio-pill" data-val="Scheduled"><input type="radio" name="status"><span class="dot"></span>Scheduled</label>
<label class="radio-pill" data-val="Issued"><input type="radio" name="status"><span class="dot"></span>Issued</label>
<label class="radio-pill" data-val="In Progress"><input type="radio" name="status"><span class="dot"></span>In progress</label>
<label class="radio-pill pill-hold" data-val="Issue"><input type="radio" name="status"><span class="dot"></span>Issue (hold)</label>
<label class="radio-pill" data-val="QC"><input type="radio" name="status"><span class="dot"></span>QC</label>
<label class="radio-pill" data-val="Closed"><input type="radio" name="status"><span class="dot"></span>Closed</label>
</div>
<div class="field-hint">Cannot move to <strong>Issued</strong> or beyond until all constraints are cleared.</div>
</div>
</div>
<div class="notice">WP number builds automatically from these scope fields + the WP type (per the SOP naming format):</div>
<div class="field-grid" id="number-dims"></div>
<div class="field field-grid col1"><div class="field"><label>Subject / title <span class="req">*</span></label><input type="text" id="wp_subject" placeholder="e.g. Utility Level 2P Inert Gas Room Wall Mount Midas/ Rack"></div></div>
<div class="field-grid">
<div class="field"><label>WP type <span class="req">*</span></label><select id="wp_type" onchange="onTypeChange()"></select><div class="field-hint sop-hint">from SOP types</div></div>
<div class="field"><label>System / facility code / UPN</label><input type="text" id="wp_system" placeholder="ties to controls.dev / COIN"></div>
<!-- Location is its own CR-006 section while still living inside General
Information's grid. CR-004 gives it structured building/floor/sector
fields of its own in wave 6; only this wrapper's contents change then. -->
<!-- CR-004. Three dependent dropdowns off the project's own taxonomy
(CR-005 / T5.4), not free text: the values are what CR-018 rolls cost
up by, and a rollup keyed on what somebody typed is not a rollup.
wp_location survives as a hidden field so a package written before
this keeps what it said — CLAUDE.md, removals are hidden not deleted. -->
<div class="field" id="location-card">
<label for="wp_building">Location<span class="help-tip" data-tip="Building, floor and sector come from this project's own location list, configured on step 11 of the SOP. Cost and progress roll up by these, which is why they are picked rather than typed.">i</span></label>
<div class="loc-picker">
<select id="wp_building" aria-label="Building"></select>
<select id="wp_floor" aria-label="Floor"></select>
<select id="wp_sector" aria-label="Sector"></select>
</div>
<div class="field-hint" id="wp_location_note"></div>
<input type="hidden" id="wp_location">
</div>
<!-- CR-002 hides these two rather than deleting them: the columns, the model
and every value already captured stay exactly as they are, and another
project can turn them back on without a code change. The ids are what
WP_FIELD_NODES addresses. -->
<div class="field" id="field-costCode"><label>Cost code</label><select id="wp_cost"></select><div class="field-hint sop-hint">Acumatica cost codes</div></div>
<div class="field" id="field-acumaticaTask"><label>Acumatica task</label><input type="text" id="wp_wbs" placeholder="Acumatica task no."></div>
</div>
<div class="field-grid">
<div class="field"><label>Owner <span class="help-tip" data-tip="The accountable owner (a user account on this project). Assigning notifies them by email if email notifications are enabled in the admin console.">i</span></label><select id="wp_assignee"><option value="">— Unassigned —</option></select></div>
<div class="field"><label>Assignees<span class="help-tip" data-tip="The crew and staff working this package. Pick from the project team named on the SOP; anyone without a user account can still be added by name.">i</span></label>
<div class="people-pick" id="pick_assignees"></div>
<input type="hidden" id="wp_assignees"></div>
<div class="field"><label>Distribution<span class="help-tip" data-tip="Who gets notified about this package. The project's Construction Manager is included by default and can be removed per package.">i</span></label>
<div class="people-pick" id="pick_distribution"></div>
<input type="hidden" id="wp_distribution"></div>
<!-- CR-003. Three levels, agreed live in the meeting, and there is no fourth.
Independent of status: a package can become Urgent after it is issued
without its status moving. -->
<div class="field"><label>Priority <span class="req">*</span><span class="help-tip" data-tip="How urgent this package is, independently of its status and its due date. Normal is the baseline; High and Urgent are exceptions and are meant to stay rare.">i</span></label>
<select id="wp_priority">
<option value="Normal" selected>Normal</option>
<option value="High">High</option>
<option value="Urgent">Urgent</option>
</select></div>
<div class="field"><label>Due date</label><input type="date" id="wp_due"></div>
<!-- CR-001. Beside the due date on purpose: a date on a work package that
is not anchored to a schedule activity is a date floating on its own,
and the meeting placed the activity next to it for exactly that reason.
Free text — a validated lookup against an imported P6 activity list is
deferred (BL-000a) partly because the Micron schedule is being reworked,
and importing it now would import churn. -->
<div class="field"><label>P6 activity ID<span class="help-tip" data-tip="The Primavera P6 schedule activity this package delivers. Free text for now — a validated lookup against an imported activity list is deferred while the schedule is being reworked.">i</span></label>
<input type="text" id="wp_p6_id" placeholder="e.g. A1234"></div>
<div class="field"><label>P6 activity description</label>
<input type="text" id="wp_p6_desc" placeholder="what that activity covers"></div>
<div class="field"><label>Specification section</label>
<input type="text" id="wp_spec" readonly class="locked-field" placeholder="set on the WP type in the SOP">
<div class="field-hint" id="spec-folder-link"></div></div>
</div>
<div class="field field-grid col1"><div class="field"><label>Description</label><textarea id="wp_desc" rows="2" placeholder="Short summary of the package"></textarea></div></div>
<div class="field field-grid col1" id="bimlink-wrap"><div class="field"><label>Enabled by — BIM package(s)<span class="help-tip" data-tip="Advanced Work Packaging traceability: link the BIM / model package(s) that enabled this install package. Paste the MWP number(s) or a link to the model package.">i</span></label><input type="text" id="wp_bimlink" placeholder="e.g. MWP07-FAB-CONDUITS, or a link to the model package"></div></div>
</div>
<!-- BIM / MODEL DETAILS (shown for BIM/VDC SOPs) -->
<div class="card" id="bim-card" style="display:none">
<div class="sub-heading">BIM / Model Details</div>
<div class="notice">For BIM/VDC work packages — the model deliverable's level of detail, area, source scan, and coordination status.</div>
<div class="field-grid">
<div class="field"><label>Model area / zone</label><input type="text" id="wp_model_area" placeholder="e.g. Fab 09 Subfab — Level 2"></div>
<div class="field"><label>Clash / coordination status</label>
<select id="wp_clash" onchange="onClashChange()"><option value=""></option><option>Not started</option><option>In coordination</option><option>Clashes open</option><option>Clash-free</option><option>Signed off (IFF)</option></select></div>
<div class="field"><label>IFF #<span class="help-tip" data-tip="Issued-For-Fabrication/Field number — the GC sign-off reference for this model package. Required once the coordination status is Signed off (IFF).">i</span></label>
<input type="text" id="wp_iff" placeholder="e.g. IFF-2026-0142" oninput="onClashChange()">
<div class="field-hint" id="iff-hint"></div></div>
<div class="field"><label>Linked scan / point cloud</label><input type="url" id="wp_scan_link" placeholder="WebShare / BIM360 / SharePoint link"></div>
</div>
</div>
<!-- ASSETS (controls.dev) -->
<div class="card" id="asset-card">
<div class="sub-heading">Assets</div>
<div class="notice">Every work package is based on one or more assets managed in <strong>controls.dev</strong>. Paste the controls.dev link for each asset this package covers. <span style="color:var(--text-dim)">A direct integration to pick assets from a list is planned — for now, link them manually.</span></div>
<div class="table-wrap"><table><thead><tr><th style="width:200px">Asset Tag / ID</th><th>Description</th><th>controls.dev Link <span class="req">*</span></th><th style="width:44px"></th></tr></thead><tbody id="asset-body"></tbody></table></div>
<button class="add-btn" onclick="addAsset()">+ Add asset</button>
</div>
<!-- DISCIPLINES -->
<div class="card" id="discipline-card" style="display:none">
<div class="sub-heading">Disciplines<span class="help-tip" data-tip="Pick every discipline this package covers. Choosing two or more turns Scope into per-discipline sections and enables Split by Discipline.">i</span></div>
<div class="notice" id="discipline-note"></div>
<div class="disc-picker" id="discipline-picker"></div>
</div>
<!-- SCOPE & WORK -->
<div class="card" id="scope-card">
<div class="sub-heading">Scope & Work<span class="help-tip" data-tip="Ordered steps the crew performs. With multiple disciplines selected, each gets its own scope section and status. Use Split by Discipline to break a large package into WP01A / WP01B / WP01C instances.">i</span></div>
<div id="flat-scope">
<div class="field"><label>Description of work (sequenced steps)</label>
<div class="notice">Enter the work as ordered steps — added in sequence, the way the crew performs them.</div>
<div id="worksteps-body"></div>
<button class="add-btn" onclick="addWorkStep()">+ Add step</button>
</div>
</div>
<div id="scope-by-discipline" style="display:none"></div>
<button class="btn btn-ghost" id="split-disc-btn" style="display:none;margin-top:10px" onclick="splitByDiscipline()" title="Break this multi-discipline package into one numbered instance per discipline">⎘ Split by discipline</button>
<div class="field-grid" style="margin-top:14px">
<div class="field"><label>Labor est. hrs.</label><input type="number" id="wp_hours" min="0" step="1" placeholder="e.g. 20" oninput="onHoursChange()"><div class="field-hint" id="size-check"></div></div>
<div class="field"><label>Predecessor work packages<span class="help-tip" data-tip="The packages that must be Closed before this one can be released. A package with an open predecessor is not release-ready — you can still release it, but the override is logged.">i</span></label>
<div class="people-pick" id="pick_predecessors"></div>
<div class="field-hint" id="pred-hint"></div></div>
<div class="field"><label>Sequence phase <span class="help-tip" data-tip="Which phase of the SOP's construction sequence this package belongs to. Descriptive — it does not gate release; predecessor packages do.">i</span></label>
<select id="wp_seq"></select><div class="field-hint sop-hint">from the SOP construction sequence</div></div>
</div>
</div>
<!-- MATERIAL LIST -->
<div class="card" id="material-card">
<div class="sub-heading">Material List<span class="help-tip" data-tip="Bill of materials — feeds kitting. On a multi-discipline package each line can be tagged to a discipline so a split routes each instance only its own materials. Import from CSV is supported.">i</span></div>
<div class="notice">Structured bill of materials. Feeds kitting and the delivery forecast. Unit is from the Acumatica unit list.</div>
<div class="table-wrap"><table><thead><tr><th style="width:90px">Qty</th><th style="width:120px">Unit</th><th>Description</th><th id="mat-disc-th" style="width:140px;display:none">Discipline</th><th style="width:44px"></th></tr></thead><tbody id="material-body"></tbody></table></div>
<div class="material-actions">
<button class="add-btn" onclick="addMaterial()">+ Add material line</button>
<button class="add-btn" onclick="document.getElementById('material-import').click()">⤒ Import from Excel/CSV</button>
<button class="add-btn" onclick="downloadMaterialTemplate()">⤓ Download template</button>
<input type="file" id="material-import" accept=".csv,.xlsx,.xls" style="display:none" onchange="importMaterials(event)">
</div>
</div>
<!-- DRAWINGS / ATTACHMENTS -->
<div class="card" id="drawings-card">
<div class="sub-heading">Drawings & Attachments</div>
<div id="sop-ref-links" class="sop-ref-links"></div>
<div class="table-wrap"><table><thead><tr><th>Document / Drawing</th><th style="width:90px">Rev</th><th>Link / Note</th><th style="width:44px"></th></tr></thead><tbody id="attach-body"></tbody></table></div>
<button class="add-btn" onclick="addAttach()">+ Add document</button>
<button class="add-btn" onclick="toggleSopFilePanel()">+ Add files from SOP folder</button>
<div id="sop-file-panel" style="display:none; margin-top:0.75rem; padding:0.75rem; border:1px dashed var(--border); border-radius:6px; background:var(--bg);">
<div id="sop-file-folders"></div>
<div class="field-hint" style="margin-top:0.5rem;">2) Paste the file links here, one per line:</div>
<textarea id="sop-file-links" rows="4" placeholder="https://primecontrolsdallas.sharepoint.com/:b:/r/.../Drawings/EE-1YA-2P.pdf?csf=1&amp;web=1&amp;e=..." style="width:100%; font-size:12px; padding:0.5rem; border:1px solid var(--border); border-radius:4px; box-sizing:border-box;"></textarea>
<div style="margin-top:0.5rem; display:flex; gap:0.5rem;">
<button class="add-btn" onclick="addPastedFileLinks()">Add to attachments</button>
<button class="add-btn" onclick="toggleSopFilePanel()" style="background:transparent;">Cancel</button>
</div>
</div>
</div>
<!-- KITTING & MIMO -->
<div class="card" id="mimo-card">
<div class="sub-heading">Kitting & Material Movement (MIMO)</div>
<div class="field-grid">
<div class="field"><label>Kitting status</label>
<select id="wp_kit_status"><option value=""></option><option>Open</option><option>In Progress</option><option>Kitted</option><option>Delivered</option></select></div>
<div class="field"><label>Warehouse owner</label><input type="text" id="wp_kit_owner" placeholder="name"></div>
<div class="field"><label>Kitting need date</label><input type="date" id="wp_kit_date"></div>
<div class="field"><label>MIMO sch. time</label><input type="datetime-local" id="wp_mimo_time"><div class="field-hint">scheduled material-move date &amp; time</div></div>
<div class="field"><label>MIMO location</label><input type="text" id="wp_mimo_loc" placeholder="staging / move location"></div>
</div>
</div>
<!-- CONSTRAINTS / RELEASE READINESS -->
<div class="card" id="constraint-card">
<div class="sub-heading">Constraints — Release Readiness<span class="help-tip" data-tip="A package can't be Issued until every constraint is Cleared or N/A. If one reopens after release, the package drops to Issue (Hold).">i</span></div>
<div class="notice">Per AWP, a package is not released to the field until every constraint is <strong>Cleared</strong> or <strong>N/A</strong>. If a constraint reopens after release, status drops to <strong>Issue (Hold)</strong>.</div>
<div class="table-wrap"><table><thead><tr><th>Constraint</th><th style="width:230px">Status</th><th>Comment</th></tr></thead><tbody id="constraint-body"></tbody></table></div>
</div>
<!-- QUALITY & HOLD POINTS -->
<div class="card" id="quality-card">
<div class="sub-heading">Quality, Inspection & Hold Points</div>
<div class="field-grid">
<div class="field"><label>QC required</label><input type="text" id="wp_qc" placeholder="from SOP" readonly>
<div class="lock-row"><button type="button" class="lock-edit" onclick="editQuality('wp_qc')">🔒 Edit (reason required)</button><span class="override-note"></span></div></div>
<div class="field"><label>Photo documentation</label><input type="text" id="wp_photo" placeholder="from SOP" readonly>
<div class="lock-row"><button type="button" class="lock-edit" onclick="editQuality('wp_photo')">🔒 Edit (reason required)</button><span class="override-note"></span></div></div>
</div>
<div class="field field-grid col1"><div class="field"><label>Witness / hold points</label><textarea id="wp_hold" rows="2" readonly placeholder="from SOP"></textarea>
<div class="lock-row"><button type="button" class="lock-edit" onclick="editQuality('wp_hold')">🔒 Edit (reason required)</button><span class="override-note"></span></div>
<div class="field-hint">Inherited from the SOP. A <strong>Hold Point</strong> stops work until inspection sign-off; a <strong>Witness Point</strong> is offered for inspection but work may proceed if declined.</div></div></div>
</div>
<!-- SIGN-OFFS -->
<div class="card">
<div class="sub-heading">Approvals & Sign-offs</div>
<div class="notice">Per the AWP IWP checklist. A package should be signed by these roles before release.</div>
<div class="table-wrap"><table><thead><tr><th style="width:220px">Role</th><th>Name</th><th style="width:150px">Date</th><th style="width:80px;text-align:center">Signed</th></tr></thead><tbody id="signoff-body"></tbody></table></div>
</div>
<!-- CLOSEOUT -->
<div class="card" id="closeout-card">
<div class="sub-heading">Closeout</div>
<div class="notice">Completed at QC / Closed — captures as-built reality and lessons learned.</div>
<div class="field-grid">
<div class="field"><label>Actual hrs.</label><input type="number" id="wp_actual_hrs" min="0" step="1"></div>
<div class="field"><label>Installed quantity</label><input type="text" id="wp_installed_qty" placeholder="e.g. 42 of 42 tags"></div>
</div>
<div class="field field-grid col1"><div class="field"><label>Redlines / as-built notes</label><textarea id="wp_redlines" rows="2"></textarea></div></div>
<div class="field field-grid col1"><div class="field"><label>Lessons learned</label><textarea id="wp_lessons" rows="2"></textarea></div></div>
</div>
<div class="nav-row"><button class="btn btn-ghost" onclick="newPackage()">↺ Clear</button>
<div style="display:flex;gap:10px">
<button class="btn btn-ghost" onclick="savePackage(false)">Save draft</button>
<button class="btn btn-generate" onclick="savePackage(true)">⚡ Save &amp; view</button>
</div></div>
<!-- DASHBOARD -->
<div id="dashboard-view" style="display:none">
<div class="output-toolbar">
<button class="btn btn-ghost" onclick="showForm()">← Back to form</button>
<div style="font-weight:700;font-size:15px">Work Package Dashboard</div>
<div style="display:flex;gap:10px;margin-left:auto">
<button class="btn btn-ghost" onclick="newPackage()">+ New WP</button>
<button class="btn btn-ghost" onclick="renderDashboard()">↻ Refresh</button>
<button class="btn btn-ghost" onclick="exportPackages()">⤓ Export (JSON)</button>
</div>
</div>
<div class="ctx-bar" style="margin:0 0 14px"><div class="field-hint">Status, metrics and gating across all saved work packages on this device. <span style="color:var(--text-dim)">Reads local data now; wires to the shared SQL database in Phase 2.</span></div></div>
<div id="dash-body"></div>
</div>
<!-- OUTPUT -->
<div id="pkg-output" style="display:none">
<div class="output-toolbar">
<button class="btn btn-ghost" onclick="showForm()">← Back to form</button>
<button class="btn btn-primary" onclick="printPackage()">⎙ Print / save PDF</button>
<button class="btn btn-ghost" onclick="exportPackages()">⤓ Export (JSON)</button>
</div>
<div class="output-doc" id="pkg-doc"></div>
</div>
<!-- SAVED -->
<div class="card" id="saved-card" style="display:none">
<div class="sub-heading">Saved Work Packages <span id="saved-count"></span></div>
<div class="table-wrap"><table><thead><tr><th>WP #</th><th>Type</th><th>Subject</th><th>Status</th><th style="width:110px">Ready?</th><th style="width:120px"></th></tr></thead><tbody id="saved-body"></tbody></table></div>
<button class="add-btn" onclick="exportPackages()">⤓ Export all (JSON)</button>
<button class="add-btn" onclick="clearSaved()">Clear all</button>
</div>
</div>
</div>
<!-- HOLD LOG MODAL (comment 7) -->
<div class="modal-overlay" id="hold-modal">
<div class="modal">
<div class="modal-head"><div class="modal-title">Log Hold — On-Hold Constraint</div><button class="cmt-x" onclick="cancelHold()" title="Cancel"></button></div>
<div class="modal-body">
<div class="notice">Moving a package to <strong>Issue (Hold)</strong> requires logging the constraint that blocked it.</div>
<div class="field"><label>Constraint type <span class="req">*</span></label><select id="hold-constraint"></select></div>
<div class="field"><label>Details <span class="req">*</span></label><textarea id="hold-details" rows="3" placeholder="What reopened / blocked this package?"></textarea></div>
<div class="field"><label>Supporting document link</label><input type="text" id="hold-doclink" placeholder="link to RFI, photo, email, etc. (optional)"></div>
<div class="field"><label>Supporting photo</label><input type="file" id="hold-photo" accept="image/*" onchange="holdPhotoChange(event)"><div class="hold-photo-preview" id="hold-photo-preview"></div></div>
</div>
<div class="modal-foot"><button class="btn btn-ghost" onclick="cancelHold()">Cancel</button><button class="btn btn-generate" onclick="submitHold()">Log hold</button></div>
</div>
</div>
<!-- VIEW SOP MODAL (comment 2) -->
<div class="modal-overlay" id="sop-modal">
<div class="modal" style="max-width:640px">
<div class="modal-head"><div class="modal-title">Project SOP — Reference</div><button class="cmt-x" onclick="closeSopModal()" title="Close"></button></div>
<div class="modal-body" id="sop-modal-body"></div>
<div class="modal-foot"><button class="btn btn-primary" onclick="closeSopModal()">Close</button></div>
</div>
</div>
<!-- COMMENTS DRAWER -->
<div class="cmt-overlay" id="cmt-overlay" onclick="toggleComments()"></div>
<aside class="cmt-drawer" id="cmt-drawer" aria-hidden="true">
<div class="cmt-head"><div class="cmt-title">Review Comments</div><button class="cmt-x" onclick="toggleComments()" title="Close"></button></div>
<div class="cmt-namebar"><label>Your name</label><input type="text" id="cmt-author" placeholder="e.g. J. Park" oninput="cmtSaveAuthor(this.value)"></div>
<div class="cmt-compose"><div class="cmt-compose-label">Comment on <strong id="cmt-cur-step">this form</strong></div>
<textarea id="cmt-input" rows="3" placeholder="Add feedback…"></textarea>
<button class="btn btn-primary cmt-add" onclick="addComment()">Add comment</button></div>
<div class="cmt-list" id="cmt-list"></div>
<div class="cmt-foot"><div class="cmt-note">Comments are saved in your browser. Use <strong>Export</strong> to send feedback back; the owner can <strong>Import</strong> each file.</div>
<div class="cmt-foot-btns"><button class="btn btn-ghost" onclick="exportComments()">⤓ Export</button>
<button class="btn btn-ghost" onclick="document.getElementById('cmt-import').click()">⤒ Import</button>
<button class="btn btn-ghost cmt-clear" onclick="clearMyComments()">Clear mine</button>
<input type="file" id="cmt-import" accept="application/json" style="display:none" onchange="importComments(event)"></div></div>
</aside>
<!-- STICKY SAVE BAR (always-visible save + release status) -->
<div class="sticky-save" id="sticky-save" style="display:none">
<span class="sticky-status" id="sticky-status"></span>
<div class="sticky-actions">
<button class="btn btn-ghost" onclick="savePackage(false)">Save draft</button>
<button class="btn btn-generate" onclick="savePackage(true)">⚡ Save &amp; view</button>
</div>
</div>
<script src="feedback-config.js"></script>
<script src="project-data.js"></script>
<script src="help.js"></script>
<script src="wp-creation-app.js"></script>
<script src="wp-sidenav.js"></script>
</body>
</html>