Add discipline strategy, WP sizing, Split-by-Discipline & dashboard
SOP config (Governance step) now sets project discipline policy:
- disciplines list, discipline strategy (single/multi/planner-choice),
letter instance-suffix style, and a max-hours split threshold.
WP Creator becomes discipline-aware:
- discipline picker; selecting 2+ turns the flat scope into per-discipline
scope sections, each with its own status (rolls up to least-advanced).
- "Split by Discipline" turns a multi-discipline WP into WP01A/B/C instances
linked to a kept master (instanceOf/parentNumber/split/children).
- est-hours warning against the SOP split threshold.
New WP Dashboard (header button, home card, ?view=dashboard deep-link):
metrics, status/discipline breakdowns, a gating panel, and a filterable
board with view/edit/issue. Reads localStorage via an API-ready WPData
adapter; masters excluded from counts.
Backend: POST /api/wps/{id}/issue (enforces the constraint gate),
POST /api/wps/{id}/status, GET /api/wps/metrics; work_packages gains
parent_id + issued_at.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -10,7 +10,7 @@ let state = {
|
||||
teamMembers: [],
|
||||
signoffRoles: [{role:'Superintendent',name:''},{role:'Foreman',name:''}],
|
||||
wpTypes: [],
|
||||
governance: {woformat:'', wosize:'', issuance:[]},
|
||||
governance: {woformat:'', wosize:'', issuance:[], disciplines:['Mechanical','Electrical','Tech'], discMode:'choice', instanceSuffix:'letter', sizeHoursMax:''},
|
||||
quality: {qcreq:'', photo:'', hold:''},
|
||||
platforms: {tracking:'CxAlloy', commissioning:'CxAlloy'},
|
||||
sequence: [],
|
||||
@@ -130,10 +130,11 @@ window.addEventListener('DOMContentLoaded',()=>{
|
||||
updateStepUI();
|
||||
updateProjectDisplay();
|
||||
|
||||
// Deep-link: ?tab=sop | ?tab=wp from the home page cards.
|
||||
// Deep-link: ?tab=sop | ?tab=wp | ?view=dashboard from the home page cards.
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
const tab = params.get('tab');
|
||||
if(tab === 'wp' || tab === 'sop') switchTool(tab);
|
||||
if(params.get('view') === 'dashboard') switchTool('wp');
|
||||
else if(tab === 'wp' || tab === 'sop') switchTool(tab);
|
||||
|
||||
track('app_open');
|
||||
let _fieldTimer;
|
||||
@@ -174,6 +175,9 @@ function loadSampleData(){
|
||||
// Populate Step 5
|
||||
document.getElementById('gov_woformat').value = 'WP##-[Sector]-[TYPE]';
|
||||
document.getElementById('gov_wosize').value = '3–5 days / 40–80 hours';
|
||||
document.getElementById('gov_disciplines').value = 'Mechanical, Electrical, Tech';
|
||||
document.getElementById('gov_discmode').value = 'choice';
|
||||
document.getElementById('gov_size_hours_max').value = '120';
|
||||
|
||||
// Populate Step 6
|
||||
document.getElementById('qual_qcreq').value = 'Yes — Detailed inspection items';
|
||||
@@ -236,6 +240,9 @@ function repopulateForm(){
|
||||
if(state.signoffRoles[1]) set('role_foreman_name', state.signoffRoles[1].name);
|
||||
set('gov_woformat', state.governance.woformat);
|
||||
set('gov_wosize', state.governance.wosize);
|
||||
set('gov_disciplines', (state.governance.disciplines||[]).join(', '));
|
||||
set('gov_discmode', state.governance.discMode);
|
||||
set('gov_size_hours_max', state.governance.sizeHoursMax);
|
||||
set('qual_qcreq', state.quality.qcreq);
|
||||
set('qual_photo', state.quality.photo);
|
||||
set('qual_hold', state.quality.hold);
|
||||
@@ -277,7 +284,8 @@ function renderWPTab(){
|
||||
gate.style.display = 'none';
|
||||
frame.style.display = 'block';
|
||||
// Reload each time so the creator picks up the latest SOP from localStorage.
|
||||
frame.src = 'wp-creation-index.html?embedded=1&t=' + Date.now();
|
||||
const wantDash = new URLSearchParams(window.location.search).get('view') === 'dashboard';
|
||||
frame.src = 'wp-creation-index.html?embedded=1' + (wantDash ? '&view=dashboard' : '') + '&t=' + Date.now();
|
||||
}else{
|
||||
gate.style.display = 'block';
|
||||
frame.style.display = 'none';
|
||||
@@ -485,15 +493,19 @@ const DEFAULT_SOURCES = [
|
||||
{label:'Safety Documentation', ph:'e.g. site safety binder'}
|
||||
];
|
||||
|
||||
// Escape a value for safe use inside a double-quoted HTML attribute.
|
||||
// SharePoint "Copy Link" URLs contain & (and labels/notes may contain & " < >),
|
||||
// so attribute values must be escaped or a re-render corrupts the field.
|
||||
function escAttr(v){ return String(v==null?'':v).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"'); }
|
||||
function renderSources(){
|
||||
const container = document.getElementById('sources-list');
|
||||
if(!state.sources.length) state.sources = DEFAULT_SOURCES.map(s=>({label:s.label, system:'', notes:'', link:'', ph:s.ph}));
|
||||
container.innerHTML = state.sources.map((s,i)=>`
|
||||
<div style="display:grid; grid-template-columns:150px 150px 250px 150px 30px; gap:1rem; align-items:center; padding:1rem; background:var(--bg); border-radius:6px; margin-bottom:0.5rem; border:1px solid var(--border);">
|
||||
<input type="text" value="${s.label}" placeholder="Label" onchange="state.sources[${i}].label=this.value" style="padding:0.5rem; font-size:12px; border:1px solid var(--border); border-radius:4px;">
|
||||
<input type="text" value="${s.system}" placeholder="${s.ph||'System of record'}" onchange="state.sources[${i}].system=this.value" style="padding:0.5rem; font-size:12px; border:1px solid var(--border); border-radius:4px;">
|
||||
<input type="text" value="${s.link}" placeholder="URL" onchange="state.sources[${i}].link=this.value" style="padding:0.5rem; font-size:12px; border:1px solid var(--border); border-radius:4px;">
|
||||
<input type="text" value="${s.notes}" placeholder="Notes" onchange="state.sources[${i}].notes=this.value" style="padding:0.5rem; font-size:12px; border:1px solid var(--border); border-radius:4px;">
|
||||
<input type="text" value="${escAttr(s.label)}" placeholder="Label" onchange="state.sources[${i}].label=this.value" style="padding:0.5rem; font-size:12px; border:1px solid var(--border); border-radius:4px;">
|
||||
<input type="text" value="${escAttr(s.system)}" placeholder="${escAttr(s.ph||'System of record')}" onchange="state.sources[${i}].system=this.value" style="padding:0.5rem; font-size:12px; border:1px solid var(--border); border-radius:4px;">
|
||||
<input type="text" value="${escAttr(s.link)}" placeholder="Paste SharePoint 'Copy Link' URL" onchange="state.sources[${i}].link=this.value" style="padding:0.5rem; font-size:12px; border:1px solid var(--border); border-radius:4px;">
|
||||
<input type="text" value="${escAttr(s.notes)}" placeholder="Notes" onchange="state.sources[${i}].notes=this.value" style="padding:0.5rem; font-size:12px; border:1px solid var(--border); border-radius:4px;">
|
||||
<button style="background:var(--danger); color:white; padding:0.25rem; width:30px; height:30px; border:none; border-radius:4px; cursor:pointer; font-weight:600;" onclick="state.sources.splice(${i},1); renderSources()">✕</button>
|
||||
</div>
|
||||
`).join('');
|
||||
@@ -573,6 +585,10 @@ function collectStepData(){
|
||||
state.governance.wosize = document.getElementById('gov_wosize').value;
|
||||
const sel = document.getElementById('gov_issuance');
|
||||
state.governance.issuance = Array.from(sel.selectedOptions).map(o=>o.value);
|
||||
state.governance.disciplines = (document.getElementById('gov_disciplines').value||'')
|
||||
.split(',').map(d=>d.trim()).filter(Boolean);
|
||||
state.governance.discMode = document.getElementById('gov_discmode').value;
|
||||
state.governance.sizeHoursMax = document.getElementById('gov_size_hours_max').value;
|
||||
break;
|
||||
case 6:
|
||||
state.quality.qcreq = document.getElementById('qual_qcreq').value;
|
||||
@@ -628,7 +644,12 @@ function completeSOP(){
|
||||
governance: {
|
||||
issuance: state.governance.issuance.length ? state.governance.issuance : ['By Sector / Area'],
|
||||
woSize: state.governance.wosize,
|
||||
woFormat: state.governance.woformat
|
||||
woFormat: state.governance.woformat,
|
||||
disciplines: (state.governance.disciplines && state.governance.disciplines.length)
|
||||
? state.governance.disciplines : ['Mechanical','Electrical','Tech'],
|
||||
discMode: state.governance.discMode || 'choice',
|
||||
instanceSuffix: state.governance.instanceSuffix || 'letter',
|
||||
sizeHoursMax: state.governance.sizeHoursMax || ''
|
||||
},
|
||||
woTypes: state.wpTypes.filter(t=>t.enabled).map(t=>({
|
||||
name: t.name,
|
||||
|
||||
Reference in New Issue
Block a user