diff --git a/ONBOARDING.md b/ONBOARDING.md
index af097e2..171f605 100644
--- a/ONBOARDING.md
+++ b/ONBOARDING.md
@@ -79,6 +79,56 @@ API. The API is already built and testable on its own.
- Firewall hardening (no external calls).
- Export/Import feedback on every surface.
- Phase 1 backend (API + schema) + NGINX proxy + deploy docs.
+- **Discipline strategy, sizing, split & dashboard** (branch `feat/wp-discipline-split-dashboard`) — see below.
+
+## Discipline strategy, WP sizing, Split-by-Discipline & Dashboard
+
+Discipline came back — but as a *project policy* set in the SOP, not a fixed
+field. The Governance step (SOP config, step 5) now also captures:
+
+- **Disciplines** (default Mechanical / Electrical / Tech) — comma list.
+- **Discipline strategy** (`governance.discMode`): `single` (one discipline per
+ WP), `multi` (one WP bundles disciplines, scope split per discipline), or
+ `choice` (planner decides per package — build big, split later).
+- **Split threshold** (`governance.sizeHoursMax`) — the Creator warns when a
+ package's est. hours exceed it.
+- `governance.instanceSuffix` = `letter` (instances get A/B/C suffixes).
+
+In the **WP Creator** ([wp-creation-app.js](wp-creation-app.js)):
+
+- A **Disciplines** picker (hidden unless the SOP defines disciplines). Pick 2+
+ and the single flat work-step list becomes **per-discipline scope sections**,
+ each with its own steps and its own status (e.g. *Issued — Electrical* while
+ *Mechanical* is still *In Progress*). Overall WP status rolls up to the
+ least-advanced discipline.
+- **⎘ Split by Discipline** turns a multi-discipline package into one instance
+ per discipline: `WP01-…` → `WP01A` (Mech), `WP01B` (Elec), `WP01C` (Tech).
+ Each instance is a single-discipline WP linked to the master via `instanceOf`
+ / `parentNumber`; the master is kept as a roll-up (`split:true`, `children:[]`).
+- New per-WP fields on the saved object: `disciplines`, `scope` (`{discipline:
+ [steps]}`), `discStatus`, `instanceOf`, `instanceLabel`, `parentNumber`,
+ `split`, `children`.
+
+The **Dashboard** (📊 in the Creator header; home card *Work Package Dashboard*;
+deep-link `work-package-suite.html?view=dashboard` or `…?view=dashboard#…`):
+metrics (total / release-ready / on-hold / overdue / est vs actual hrs),
+breakdowns by status & discipline, a **gating panel** (what's blocking each
+package), and a filterable board with **view / edit / issue** per package.
+Masters are excluded from counts so split hours aren't double-counted.
+
+Data source today is `localStorage` via the `WPData` adapter in
+[wp-creation-app.js](wp-creation-app.js) — swap `list()`/`issue()`/`setStatus()`
+to `fetch('/api/wps…')` in Phase 2 and the UI is unchanged.
+
+**Backend** ([server/](server/)) gained the matching endpoints:
+`POST /api/wps/{id}/issue` (refuses if constraints are open — the AWP gate),
+`POST /api/wps/{id}/status`, and `GET /api/wps/metrics`. `work_packages` gained
+`parent_id` and `issued_at` columns.
+
+> **Migration caveat:** tables are still auto-created on startup, so the new
+> `parent_id` / `issued_at` columns appear on a **fresh** DB only. Before there's
+> real data this is fine; once there is, add Alembic (see open question #2) and
+> migrate rather than relying on `create_all`.
**Pending — Phase 2: wire the front end to the API**
- SOP: on *SOP Complete*, `POST /api/sops`; on load, `GET /api/sops/latest` to hydrate the Creator (currently uses `localStorage` key `wp_suite_sop`).
diff --git a/index.html b/index.html
index 1ef63a8..5136b02 100644
--- a/index.html
+++ b/index.html
@@ -400,6 +400,13 @@
+
+
+
Define how Work Packages are formatted, sized, and issued on this project.
+
Define how Work Packages are formatted, sized, and issued on this project. The choices here decide how the Work Package Creator behaves for every package.
Use ## for counter, [Sector] [TYPE] as variables
-
-
-
-
+
+
How will Work Packages use disciplines?
+
Decide whether a single package can carry more than one discipline (e.g. a chiller skid needing Mechanical install + Electrical wire-pull + Tech terminations), or whether each discipline gets its own package. This drives whether the Creator shows per-discipline scope sections and the Split by Discipline button.
+
+
+
+
+ Comma-separated. These appear as scope sections and instance suffixes in the Creator.
+
+
+
+
+ "Choose per package" lets the planner build a large multi-discipline package and split it later.
+
+
+
+
Work Package sizing
+
A Work Package should be a manageable, trackable chunk of work — typically a 1–2 week assignment. The Creator warns the planner when a package exceeds the ceiling so it can be broken down.
+
+
+
+
+
+
+
+
+ The Creator flags packages above this so they can be split (by discipline or scope).
+
+
diff --git a/wp-creation-app.js b/wp-creation-app.js
index 589464a..9726421 100644
--- a/wp-creation-app.js
+++ b/wp-creation-app.js
@@ -8,15 +8,16 @@ const SAMPLE_SOP = {
meta:{tool:'Work Package Configuration', sample:true},
project:{name:'Micron — INC Construction Work Packages', number:'26-67-008', client:'Micron Technology, Inc.', division:'Semiconductor', pm:'Nick Siegfried', cm:'K. Boyd', qm:'D. Nguyen', site:'Boise, ID — Fab'},
roles:[{role:'General Foreman',name:'M. Torres'},{role:'Superintendent',name:'K. Boyd'},{role:'Safety Manager / Lead',name:'A. Reyes'},{role:'Quality Manager',name:'D. Nguyen'},{role:'Planner',name:'L. Graver'}],
- governance:{ issuance:['By Sector / Area','By Discipline'], woSize:'3–5 days', woFormat:'WP##-[Sector]-[TYPE]' },
+ governance:{ issuance:['By Sector / Area','By Discipline'], woSize:'3–5 days', woFormat:'WP##-[Sector]-[TYPE]', disciplines:['Mechanical','Electrical','Tech'], discMode:'choice', instanceSuffix:'letter', sizeHoursMax:'120' },
woTypes:[
{name:'Conduit Install', enabled:true}, {name:'Tray Install', enabled:true},
{name:'Wire Pull', enabled:true}, {name:'Terminations', enabled:true},
{name:'Instrument Install', enabled:true}, {name:'Panel Install', enabled:true},
],
sources:[
- {label:'Design Drawings',system:'Procore',notes:'',link:'https://us02.procore.com/562949954073428/project/documents/folders/drawings'},
- {label:'Specifications',system:'Procore',notes:'',link:'https://us02.procore.com/562949954073428/project/documents/folders/specs'},
+ {label:'Design Drawings',system:'SharePoint',notes:'',link:'https://primecontrolsdallas.sharepoint.com/:f:/r/sites/BusinessTechnologyGroup/Shared%20Documents/Current%20projects/Project%20SDE/Piloting/Test%20Files/Drawings?csf=1&web=1&e=Hx4lZF'},
+ {label:'Data Sheets',system:'SharePoint',notes:'',link:'https://primecontrolsdallas.sharepoint.com/:f:/r/sites/BusinessTechnologyGroup/Shared%20Documents/Current%20projects/Project%20SDE/Piloting/Test%20Files/DataSheets?csf=1&web=1&e=PkPrXc'},
+ {label:'Specifications',system:'SharePoint',notes:'',link:'https://primecontrolsdallas.sharepoint.com/:f:/r/sites/BusinessTechnologyGroup/Shared%20Documents/Current%20projects/Project%20SDE/Piloting/Test%20Files/Specs?csf=1&web=1&e=ZQ19ai'},
{label:'IO List',system:'controls.dev',notes:'',link:'https://controls.dev/io'},
{label:'Cable Schedule',system:'SharePoint',notes:'',link:'https://primecontrols.sharepoint.com/cable-schedule'},
],
@@ -38,13 +39,16 @@ const COST_CODES = ['1000|Project Management','2000|Design and Development','210
const ACU_UNITS = ['EA','EACH','FT','M','METER','HR','DAYS','MINUTE','KG','LITER','CASE','LOT','LS','PK','PACK','PALLET','PIECE','BOTTLE','CAN'];
// Example built work package (comment 4 / "Load Example") — WP02 export
-const EXAMPLE_PKG = {"number":"WP02-1P-ELEC-Conduit","status":"Issue","subject":"FAB 1P Horn Strobe Conduit","type":"Conduit Install","system":"Chilled Water","location":"FAB / LVL 1 / Sect P","cost":"4060","wbs":"2001","assignees":"David Velazquez (Catapult Solutions Group), Jesus Casiano-Figueroa (Prime Controls)","distribution":"Bill Clarida (Prime Controls), Sean Tolley (Prime Controls), Jefferson Dufriend (Prime Controls)","due":"2026-06-18","spec":"26_05_33_31 - Conduit","desc":"1P Conduit run for horns and strobes","work":"Layout conduit route\nUsing ladders and MEWP install conduit and boxes with proper strapping, supports, and pull points according to 2D drawings and 3D model. Reference package drawings included in attachments.\nWhen complete initiate inspection with Prime QAQC","workSteps":["Layout conduit route","Using ladders and MEWP install conduit and boxes with proper strapping, supports, and pull points according to 2D drawings and 3D model. Reference package drawings included in attachments.","When complete initiate inspection with Prime QAQC"],"numberDims":{"Sector":"1P","Discipline":"ELEC"},"hours":"60","seq":"Layout","materials":[{"qty":"400","unit":"FT","desc":"3/4\" EMT"},{"qty":"300","unit":"FT","desc":"1\" EMT"},{"qty":"50","unit":"FT","desc":"7/8\" strut"},{"qty":"50","unit":"FT","desc":"1-5/8\" strut"},{"qty":"8","unit":"EA","desc":"4x4x4 NEMA 3 Box"},{"qty":"2","unit":"EA","desc":"1\" C-Type Conduit Body"},{"qty":"1","unit":"EA","desc":"1\" T-Type Conduit Body"},{"qty":"2","unit":"EA","desc":"3/4\" C-Type Conduit Body"},{"qty":"2","unit":"EA","desc":"3/4\" T-Type Conduit Body"},{"qty":"3","unit":"EA","desc":"3/4\" EMT LB & Cover"},{"qty":"6","unit":"EA","desc":"Hoffman F44GCPNK Horn Strobe Box"},{"qty":"2","unit":"EA","desc":"EMO 2x4 Box"},{"qty":"4","unit":"EA","desc":"1\" Bond Bushing w/ Lug"},{"qty":"4","unit":"EA","desc":"3/4\" Bond Bushing w/ Lug"},{"qty":"50","unit":"EA","desc":"1/4\"x3\" Toggle Bolt"},{"qty":"100","unit":"EA","desc":"Drywall Anchor"},{"qty":"5","unit":"EA","desc":"1\" to 3/4\" threaded reducer"}],"attachments":[{"doc":"EE-1YA-2P-5_ HPM PANEL CALLOUT 05","rev":"0","link":"https://us02.procore.com/webclients/host/companies/562949953431440/projects/562949954073428/tools/document-viewer/prostore/562950644886790"}],"kitStatus":"Open","kitOwner":"Ian Spielburg","kitDate":"2026-06-15","mimoTime":"2026-06-11T10:30","mimoLoc":"04","constraints":[{"name":"Safety & Permitting","status":"cleared","comment":""},{"name":"Quality Control / Inspection","status":"cleared","comment":""},{"name":"IFC Drawings & Specs","status":"cleared","comment":""},{"name":"Schedule","status":"cleared","comment":""},{"name":"Materials (on site, bagged & tagged)","status":"cleared","comment":""},{"name":"Prefabrication","status":"cleared","comment":""},{"name":"Work Access & Laydown","status":"cleared","comment":""},{"name":"Craft Availability","status":"cleared","comment":""},{"name":"Construction Equipment & Tools","status":"open","comment":"Boom lift not yet on site"},{"name":"Scaffolding / Access Equipment","status":"cleared","comment":""}],"qc":"Yes — Detailed inspection items","photo":"Key checkpoints only","hold":"HOLD: Prime QAQC to inspect rough-in before cover/cover-up. WITNESS: client QC to observe megger / insulation-resistance test before energization.","overrides":{"wp_photo":"Change Order"},"signoffs":[{"role":"Planner","name":"L. Graver","date":"2026-06-10","signed":true,"dateReason":""},{"role":"Superintendent","name":"K. Boyd","date":"2026-06-10","signed":true,"dateReason":""},{"role":"HSE Professional","name":"A. Reyes","date":"","signed":false,"dateReason":""},{"role":"Quality Representative","name":"D. Nguyen","date":"","signed":false,"dateReason":""},{"role":"Work Foreman","name":"M. Torres","date":"","signed":false,"dateReason":""}],"holds":[],"actualHrs":"54","installedQty":"200 ft","redlines":"Conduit size changed. need to update model","lessons":"Prepping trapeze hangers with conduit straps saved time","project":"Micron — INC Construction Work Packages","track":"CxAlloy"};
+const EXAMPLE_PKG = {"number":"WP02-1P-ELEC-Conduit","status":"Issue","subject":"FAB 1P Horn Strobe Conduit","type":"Conduit Install","system":"Chilled Water","location":"FAB / LVL 1 / Sect P","cost":"4060","wbs":"2001","assignees":"David Velazquez (Catapult Solutions Group), Jesus Casiano-Figueroa (Prime Controls)","distribution":"Bill Clarida (Prime Controls), Sean Tolley (Prime Controls), Jefferson Dufriend (Prime Controls)","due":"2026-06-18","spec":"26_05_33_31 - Conduit","desc":"1P Conduit run for horns and strobes","assets":[{"tag":"1P-HS-NORTH","desc":"1P North horn/strobe circuit","link":"https://controls.dev/assets/1P-HS-NORTH"}],"work":"Layout conduit route\nUsing ladders and MEWP install conduit and boxes with proper strapping, supports, and pull points according to 2D drawings and 3D model. Reference package drawings included in attachments.\nWhen complete initiate inspection with Prime QAQC","workSteps":["Layout conduit route","Using ladders and MEWP install conduit and boxes with proper strapping, supports, and pull points according to 2D drawings and 3D model. Reference package drawings included in attachments.","When complete initiate inspection with Prime QAQC"],"numberDims":{"Sector":"1P","Discipline":"ELEC"},"hours":"60","seq":"Layout","materials":[{"qty":"400","unit":"FT","desc":"3/4\" EMT"},{"qty":"300","unit":"FT","desc":"1\" EMT"},{"qty":"50","unit":"FT","desc":"7/8\" strut"},{"qty":"50","unit":"FT","desc":"1-5/8\" strut"},{"qty":"8","unit":"EA","desc":"4x4x4 NEMA 3 Box"},{"qty":"2","unit":"EA","desc":"1\" C-Type Conduit Body"},{"qty":"1","unit":"EA","desc":"1\" T-Type Conduit Body"},{"qty":"2","unit":"EA","desc":"3/4\" C-Type Conduit Body"},{"qty":"2","unit":"EA","desc":"3/4\" T-Type Conduit Body"},{"qty":"3","unit":"EA","desc":"3/4\" EMT LB & Cover"},{"qty":"6","unit":"EA","desc":"Hoffman F44GCPNK Horn Strobe Box"},{"qty":"2","unit":"EA","desc":"EMO 2x4 Box"},{"qty":"4","unit":"EA","desc":"1\" Bond Bushing w/ Lug"},{"qty":"4","unit":"EA","desc":"3/4\" Bond Bushing w/ Lug"},{"qty":"50","unit":"EA","desc":"1/4\"x3\" Toggle Bolt"},{"qty":"100","unit":"EA","desc":"Drywall Anchor"},{"qty":"5","unit":"EA","desc":"1\" to 3/4\" threaded reducer"}],"attachments":[{"doc":"EE-1YA-2P-5_ HPM PANEL CALLOUT 05","rev":"0","link":"https://us02.procore.com/webclients/host/companies/562949953431440/projects/562949954073428/tools/document-viewer/prostore/562950644886790"}],"kitStatus":"Open","kitOwner":"Ian Spielburg","kitDate":"2026-06-15","mimoTime":"2026-06-11T10:30","mimoLoc":"04","constraints":[{"name":"Safety & Permitting","status":"cleared","comment":""},{"name":"Quality Control / Inspection","status":"cleared","comment":""},{"name":"IFC Drawings & Specs","status":"cleared","comment":""},{"name":"Schedule","status":"cleared","comment":""},{"name":"Materials (on site, bagged & tagged)","status":"cleared","comment":""},{"name":"Prefabrication","status":"cleared","comment":""},{"name":"Work Access & Laydown","status":"cleared","comment":""},{"name":"Craft Availability","status":"cleared","comment":""},{"name":"Construction Equipment & Tools","status":"open","comment":"Boom lift not yet on site"},{"name":"Scaffolding / Access Equipment","status":"cleared","comment":""}],"qc":"Yes — Detailed inspection items","photo":"Key checkpoints only","hold":"HOLD: Prime QAQC to inspect rough-in before cover/cover-up. WITNESS: client QC to observe megger / insulation-resistance test before energization.","overrides":{"wp_photo":"Change Order"},"signoffs":[{"role":"Planner","name":"L. Graver","date":"2026-06-10","signed":true,"dateReason":""},{"role":"Superintendent","name":"K. Boyd","date":"2026-06-10","signed":true,"dateReason":""},{"role":"HSE Professional","name":"A. Reyes","date":"","signed":false,"dateReason":""},{"role":"Quality Representative","name":"D. Nguyen","date":"","signed":false,"dateReason":""},{"role":"Work Foreman","name":"M. Torres","date":"","signed":false,"dateReason":""}],"holds":[],"actualHrs":"54","installedQty":"200 ft","redlines":"Conduit size changed. need to update model","lessons":"Prepping trapeze hangers with conduit straps saved time","project":"Micron — INC Construction Work Packages","track":"CxAlloy"};
// ── STATE ────────────────────────────────────────────────────────────────────
let SOP=null, editingId=null, numberDirty=false;
-let pkgMaterials=[], pkgAttach=[], pkgConstraints=[], pkgSignoffs=[], pkgHolds=[], pkgWorkSteps=[];
+let pkgMaterials=[], pkgAttach=[], pkgConstraints=[], pkgSignoffs=[], pkgHolds=[], pkgWorkSteps=[], pkgAssets=[];
let pkgOverrides={}; // {fieldId: reason} for SOP-locked fields that were edited
let numberDims={}; // {Sector:'', Discipline:''} dimensions that build the WP number (comment 3)
+let pkgDisciplines=[]; // disciplines this WP covers (from SOP governance.disciplines)
+let pkgScope={}; // {discipline:[steps]} — per-discipline scope when multi-discipline
+let pkgDiscStatus={}; // {discipline:status} — per-discipline phase tracking (e.g. Issued-Electrical)
let prevStatus='Draft';
let devMode=false; // comment 7: pauses usage tracking during review
let savedPackages=[];
@@ -76,6 +80,7 @@ function importSOP(ev){
}
function applySOP(){
renderCtxBar(); buildTypePicker(); buildCostCodes(); buildSequencePicker(); buildNumberDims();
+ buildDisciplinePicker(); renderScope(); onHoursChange();
renderSopRefLinks(); renderSpecFolderLink();
// SOP-inherited Quality fields — populated then locked (editable only with a logged reason)
if(SOP.quality){
@@ -88,6 +93,7 @@ function applySOP(){
buildConstraints(); buildSignoffs();
if(!pkgMaterials.length){ pkgMaterials=[{qty:'',unit:'',desc:''}]; buildMaterials(); }
if(!pkgAttach.length){ pkgAttach=[{doc:'',rev:'',link:''}]; buildAttach(); }
+ if(!pkgAssets.length){ pkgAssets=[{tag:'',desc:'',link:''}]; buildAssets(); }
if(!pkgWorkSteps.length){ pkgWorkSteps=['']; buildWorkSteps(); }
updateNumber(); updateReleaseBanner();
}
@@ -262,6 +268,168 @@ function buildWorkSteps(){
function addWorkStep(){ pkgWorkSteps.push(''); buildWorkSteps(); }
function removeWorkStep(i){ pkgWorkSteps.splice(i,1); if(!pkgWorkSteps.length)pkgWorkSteps=['']; buildWorkSteps(); }
+// ── DISCIPLINES, PER-DISCIPLINE SCOPE & STATUS ───────────────────────────────
+// A project's SOP decides how WPs use disciplines (governance.discMode):
+// 'single' one discipline per WP 'multi' one WP carries several disciplines
+// 'choice' planner decides per package (build big, split later)
+function sopDisciplines(){ const g=(SOP&&SOP.governance)||{}; return (g.disciplines&&g.disciplines.length)?g.disciplines:['Mechanical','Electrical','Tech']; }
+function sopDiscMode(){ return (SOP&&SOP.governance&&SOP.governance.discMode)||'choice'; }
+function disciplinesConfigured(){ return !!(SOP&&SOP.governance&&Array.isArray(SOP.governance.disciplines)&&SOP.governance.disciplines.length); }
+function isMultiDiscipline(){ return pkgDisciplines.length>1; }
+function instanceSuffixStyle(){ return (SOP&&SOP.governance&&SOP.governance.instanceSuffix)||'letter'; }
+
+function buildDisciplinePicker(){
+ const wrap=document.getElementById('discipline-picker'); if(!wrap) return;
+ const card=document.getElementById('discipline-card');
+ const list=sopDisciplines();
+ if(!disciplinesConfigured() || !list.length){ if(card) card.style.display='none'; return; }
+ if(card) card.style.display='';
+ const mode=sopDiscMode();
+ const note=document.getElementById('discipline-note');
+ if(note){
+ note.textContent = mode==='single'
+ ? 'This project issues one discipline per package — choose the single discipline.'
+ : mode==='multi'
+ ? 'This project bundles disciplines into one package — select every discipline this package covers; each gets its own scope section.'
+ : 'Select every discipline this package covers. Choose more than one to build a combined package you can split later.';
+ }
+ wrap.innerHTML = list.map(d=>{
+ const on=pkgDisciplines.includes(d);
+ return ``;
+ }).join('');
+}
+function toggleDiscipline(d,on){
+ if(sopDiscMode()==='single'){ pkgDisciplines = on?[d]:[]; }
+ else { if(on){ if(!pkgDisciplines.includes(d)) pkgDisciplines.push(d); } else { pkgDisciplines=pkgDisciplines.filter(x=>x!==d); } }
+ // keep per-discipline scope/status maps in sync with the selection
+ pkgDisciplines.forEach(x=>{ if(!pkgScope[x]) pkgScope[x]=['']; if(!pkgDiscStatus[x]) pkgDiscStatus[x]=getRadio('status')||'Draft'; });
+ Object.keys(pkgScope).forEach(x=>{ if(!pkgDisciplines.includes(x)) delete pkgScope[x]; });
+ Object.keys(pkgDiscStatus).forEach(x=>{ if(!pkgDisciplines.includes(x)) delete pkgDiscStatus[x]; });
+ buildDisciplinePicker(); renderScope(); track('discipline_toggle',{count:pkgDisciplines.length});
+}
+
+// Show the flat work-step list for 0–1 disciplines; per-discipline sections for 2+.
+function renderScope(){
+ const flat=document.getElementById('flat-scope');
+ const multi=document.getElementById('scope-by-discipline');
+ const splitBtn=document.getElementById('split-disc-btn');
+ if(!multi) return;
+ if(isMultiDiscipline()){
+ if(flat) flat.style.display='none';
+ multi.style.display='';
+ if(splitBtn) splitBtn.style.display='';
+ buildScopeGroups();
+ } else {
+ if(flat) flat.style.display='';
+ multi.style.display='none';
+ multi.innerHTML='';
+ if(splitBtn) splitBtn.style.display='none';
+ }
+}
+function buildScopeGroups(){
+ const multi=document.getElementById('scope-by-discipline'); if(!multi) return;
+ multi.innerHTML = pkgDisciplines.map(d=>{
+ const steps=pkgScope[d]&&pkgScope[d].length?pkgScope[d]:[''];
+ pkgScope[d]=steps;
+ const st=pkgDiscStatus[d]||'Draft';
+ const rows=steps.map((s,i)=>`
`;
+ }).join('');
+}
+function setScopeStep(d,i,v){ if(!pkgScope[d])pkgScope[d]=['']; pkgScope[d][i]=v; }
+function addScopeStep(d){ if(!pkgScope[d])pkgScope[d]=['']; pkgScope[d].push(''); buildScopeGroups(); }
+function removeScopeStep(d,i){ if(!pkgScope[d])return; pkgScope[d].splice(i,1); if(!pkgScope[d].length)pkgScope[d]=['']; buildScopeGroups(); }
+function setDiscStatus(d,v){ pkgDiscStatus[d]=v; rollupDisciplineStatus(); track('disc_status',{discipline:d,status:v}); }
+// Overall WP status rolls up to the least-advanced discipline (so a WP isn't "Closed" while a discipline lags).
+function rollupDisciplineStatus(){
+ if(!isMultiDiscipline()) return;
+ let minIdx=STATUS_ORDER.length-1;
+ pkgDisciplines.forEach(d=>{ const ix=STATUS_ORDER.indexOf(pkgDiscStatus[d]||'Draft'); if(ix>=0&&ixmax){
+ el.innerHTML=`⚠ ${hrs} hrs exceeds the ${max}-hr split threshold — consider breaking this package down`+(isMultiDiscipline()?' (try Split by Discipline).':'.')+``;
+ } else if(max){ el.textContent=`Split threshold: ${max} hrs (from SOP).`; }
+ else { el.textContent=''; }
+}
+
+// ── SPLIT BY DISCIPLINE ──────────────────────────────────────────────────────
+// Turns a multi-discipline package into one instance per discipline:
+// WP01-… → WP01A-… (Mechanical), WP01B-… (Electrical), WP01C-… (Tech)
+// Each instance is a single-discipline package linked back to the master.
+function instanceSuffixFor(index, discipline){
+ if(instanceSuffixStyle()==='discipline'){ return '_'+typeNumberCode(discipline); }
+ return String.fromCharCode(65+index); // A, B, C…
+}
+function splitByDiscipline(){
+ if(!isMultiDiscipline()){ alert('Select two or more disciplines before splitting.'); return; }
+ if(!gv('wp_subject')){ alert('Add a Subject before splitting.'); return; }
+ const base=collectPackage();
+ const baseNumber=base.number||('WP'+pad2(editingSeq()));
+ if(!confirm(`Split "${baseNumber}" into ${pkgDisciplines.length} discipline instances (${pkgDisciplines.map((d,i)=>baseNumber+instanceSuffixFor(i,d)).join(', ')})?\n\nThe master package is kept as a roll-up; each instance becomes its own single-discipline package.`)) return;
+
+ // Master: flagged as a split container, keeps all disciplines for roll-up tracking.
+ const masterId = editingId || base.id;
+ const master = {...base, id:masterId, split:true, children:[]};
+
+ const children = pkgDisciplines.map((d,i)=>{
+ const suffix=instanceSuffixFor(i,d);
+ const steps=(pkgScope[d]||[]).map(s=>s.trim()).filter(Boolean);
+ const childId='wp_'+Date.now().toString(36)+Math.random().toString(36).slice(2,5)+i;
+ master.children.push(childId);
+ return {
+ ...base,
+ id: childId,
+ number: baseNumber+suffix,
+ disciplines:[d],
+ scope:{[d]:steps.length?steps:['']},
+ discStatus:{[d]: (pkgDiscStatus[d]||base.status||'Draft')},
+ status: pkgDiscStatus[d]||base.status||'Draft',
+ workSteps: steps, work: steps.join('\n'),
+ instanceOf: masterId, instanceLabel: suffix, parentNumber: baseNumber,
+ split:false, children:undefined,
+ updatedAt:new Date().toISOString()
+ };
+ });
+
+ // Upsert master + children into the saved store.
+ const upsert=(p)=>{ const ix=savedPackages.findIndex(x=>x.id===p.id); if(ix>=0) savedPackages[ix]=p; else savedPackages.push(p); };
+ upsert(master); children.forEach(upsert);
+ editingId=masterId; saveStore(); renderSavedList();
+ track('wp_split',{disciplines:pkgDisciplines.length});
+ toast('Split into '+children.length+' discipline instances');
+ alert('Created '+children.length+' instances:\n\n• '+children.map(c=>c.number+' ('+c.disciplines[0]+')').join('\n• ')+'\n\nThe master '+baseNumber+' is kept as a roll-up. Edit each instance from the Saved Work Packages list.');
+}
+
+// ── ASSETS (controls.dev) ────────────────────────────────────────────────────
+// Interim: assets are linked manually back to controls.dev. A future direct
+// integration will let the user pick them from a list instead of pasting links.
+function buildAssets(){
+ const tb=document.getElementById('asset-body'); if(!tb) return; tb.innerHTML='';
+ pkgAssets.forEach((a,i)=>{ const tr=document.createElement('tr');
+ tr.innerHTML=`
+
+
+
`;
+ tb.appendChild(tr); });
+}
+function addAsset(){ pkgAssets.push({tag:'',desc:'',link:''}); buildAssets(); track('asset_added'); }
+function removeAsset(i){ pkgAssets.splice(i,1); if(!pkgAssets.length)pkgAssets=[{tag:'',desc:'',link:''}]; buildAssets(); }
+
// ── ATTACHMENTS ──────────────────────────────────────────────────────────────
function buildAttach(){
const tb=document.getElementById('attach-body'); tb.innerHTML='';
@@ -275,6 +443,44 @@ function buildAttach(){
function addAttach(){ pkgAttach.push({doc:'',rev:'',link:''}); buildAttach(); }
function removeAttach(i){ pkgAttach.splice(i,1); if(!pkgAttach.length)pkgAttach=[{doc:'',rev:'',link:''}]; buildAttach(); }
+// ── ADD FILES FROM SOP FOLDER (no-auth interim) ──────────────────────────────
+// Opens the SOP source folders, then turns pasted SharePoint file links into
+// attachment rows with the file name parsed from the URL. (A true embedded
+// picker needs an Azure AD app registration — see DEPLOYMENT.md.)
+function fileNameFromUrl(url){
+ try{
+ const path=String(url).split('?')[0].split('#')[0];
+ const seg=decodeURIComponent(path.split('/').filter(Boolean).pop()||'');
+ // Only use it as a doc name if it looks like a real file (has an extension);
+ // short "/:f:/s/" sharing links have no filename, so leave doc blank.
+ return /\.[a-z0-9]{2,6}$/i.test(seg) ? seg : '';
+ }catch(e){ return ''; }
+}
+function renderSopFileFolders(){
+ const box=document.getElementById('sop-file-folders'); if(!box) return;
+ const srcs=sopLinkedSources();
+ box.innerHTML = srcs.length
+ ? `
1) Open a folder, multi-select files in SharePoint, then use Copy link:
`;
+ document.getElementById('dash-body').innerHTML=h;
+}
+function dashIssue(id){
+ const p=WPData.get(id); if(!p) return;
+ if(wpOpenConstraints(p).length>0){ alert('Cannot issue — open constraints remain.'); return; }
+ if(!confirm('Issue work package "'+(p.number||p.subject)+'"? This marks it released to the field.')) return;
+ WPData.issue(id); renderDashboard(); renderSavedList(); toast('Issued '+(p.number||'')); track('dashboard_issue');
+}
+function dashView(i){ if(savedPackages[i]) renderPackage(savedPackages[i]); }
+function dashEdit(i){ const p=savedPackages[i]; if(!p) return; editingId=p.id; loadPackageIntoForm(p); }
+
// ── VIEW SOP REFERENCE (comment 2) ───────────────────────────────────────────
function openSopModal(){
if(!SOP){ alert('No SOP loaded.'); return; }
@@ -666,4 +1012,7 @@ loadStore();
setRadio('status','Draft');
renderSavedList();
cmtInit();
+// Deep-link: open straight to the dashboard when requested (?view=dashboard or #dashboard).
+(function(){ const p=new URLSearchParams(location.search); if(p.get('view')==='dashboard' || location.hash==='#dashboard'){ showDashboard(); } })();
+window.addEventListener('hashchange',()=>{ if(location.hash==='#dashboard') showDashboard(); });
track('app_open');
diff --git a/wp-creation-index.html b/wp-creation-index.html
index 96b0d9b..2f21596 100644
--- a/wp-creation-index.html
+++ b/wp-creation-index.html
@@ -24,6 +24,7 @@
+
@@ -76,16 +77,35 @@
+
+
+
Assets
+
Every work package is based on one or more assets managed in controls.dev. Paste the controls.dev link for each asset this package covers. A direct integration to pick assets from a list is planned — for now, link them manually.
+
Asset Tag / ID
Description
controls.dev Link *
+
+
+
+
+
+
Disciplines
+
+
+
+
Scope & Work
-
-
Enter the work as ordered steps — added in sequence, the way the crew performs them.
-
-
+
+
+
Enter the work as ordered steps — added in sequence, the way the crew performs them.
+
+
+
-
-
+
+
+
+
The package/step (from the SOP sequence) that must finish before this work can start. Choose "None" if it has no predecessor.
@@ -109,6 +129,16 @@
Document / Drawing
Rev
Link / Note
+
+
+
+
2) Paste the file links here, one per line:
+
+
+
+
+
+
@@ -170,6 +200,21 @@
+
+
+
+
+
Work Package Dashboard
+
+
+
+
+
+
+
Status, metrics and gating across all saved work packages on this device. Reads local data now; wires to the shared SQL database in Phase 2.