BIM as a per-package kind; one project flows BIM -> construction

Replace the whole-project BIM 'mode' with an opt-in capability + a
per-package kind, so a single project can produce both model and
install packages (and install-only projects are unaffected).

SOP tool:
- Step 4 "Include BIM / VDC work packages" checkbox (state.bimEnabled).
  Enabling adds BIM package types + BIM release gates (flagged bim) plus
  BIM roles/sources/process steps alongside the construction defaults;
  disabling strips the bim-flagged items.
- Generated SOP carries bimEnabled and a per-type / per-constraint bim flag.
- Required sign-off role titles stay editable (no longer force-renamed).

Work Package Creator:
- Shows a Package Type selector (Install IWP / BIM EWP) only when the
  SOP has bimEnabled; kind is saved per package and labeled in the output.
- WP types and release gates are filtered by kind (BIM types+gates for
  EWP, install types+gates for IWP).
- EWP reveals the BIM Details card and hides controls.dev Assets /
  Materials / Kitting-MIMO; IWP shows those plus the "Enabled by - BIM
  package" traceability link.

Supersedes the earlier whole-project BIM mode.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-30 17:07:08 -07:00
parent 566ace9969
commit 0e698438a2
4 changed files with 107 additions and 64 deletions

View File

@@ -43,6 +43,7 @@ const EXAMPLE_PKG = {"number":"WP02-1P-ELEC-Conduit","status":"Issue","subject":
// ── STATE ────────────────────────────────────────────────────────────────────
let SOP=null, editingId=null, numberDirty=false;
let pkgKind='iwp'; // 'iwp' (install) | 'ewp' (BIM) — per-package, only relevant when SOP.bimEnabled
let activeProjectId=''; // set at boot from ?project=<id>; stamped onto saved WPs for the API
let pkgMaterials=[], pkgAttach=[], pkgConstraints=[], pkgSignoffs=[], pkgHolds=[], pkgWorkSteps=[], pkgAssets=[];
let pkgOverrides={}; // {fieldId: reason} for SOP-locked fields that were edited
@@ -67,7 +68,12 @@ function toast(msg){ let t=document.getElementById('toast'); if(!t){ t=document.
function getRadio(name){ const s=document.querySelector(`.radio-pill.selected input[name="${name}"]`); return s?.closest('.radio-pill')?.dataset?.val||''; }
function setRadio(name,val){ document.querySelectorAll(`.radio-pill input[name="${name}"]`).forEach(i=>{const p=i.closest('.radio-pill'); const on=p.dataset.val===val; p.classList.toggle('selected',on); i.checked=on;}); }
function enabledTypes(){ return ((SOP&&SOP.woTypes)||[]).filter(t=>t.enabled!==false); }
function constraintNames(){ return (SOP&&Array.isArray(SOP.constraints)&&SOP.constraints.length)?SOP.constraints:DEFAULT_CONSTRAINTS; }
function constraintNames(){
let cs = (SOP&&Array.isArray(SOP.constraints)&&SOP.constraints.length)?SOP.constraints:DEFAULT_CONSTRAINTS;
// On a BIM-enabled project, EWPs use the BIM gates and IWPs use the install gates.
if(bimSOP()) cs = cs.filter(c => (c && typeof c==='object') ? (isEwp() ? c.bim : !c.bim) : !isEwp());
return cs;
}
function nextSeq(){ return savedPackages.length+1; }
// ── SOP LOADING ──────────────────────────────────────────────────────────────
@@ -92,23 +98,38 @@ function applySOP(){
lockQuality('wp_qc'); lockQuality('wp_photo'); lockQuality('wp_hold');
const hint=document.getElementById('wp_number_hint'); hint.textContent = SOP.governance&&SOP.governance.woFormat ? 'auto-built · format: '+SOP.governance.woFormat : '';
buildConstraints(); buildSignoffs();
applyBimMode();
applyKind();
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();
}
// In a BIM/VDC SOP, hide the field-only sections (controls.dev assets, materials,
// kitting/MIMO) and reveal the BIM Details card. Driven by SOP.mode==='bim'.
function isBimSOP(){ return !!(SOP && SOP.mode === 'bim'); }
function applyBimMode(){
const bim = isBimSOP();
// Per-package kind. A project whose SOP has bimEnabled produces both install (IWP)
// and BIM (EWP) packages; the kind selector tailors which fields, WP types, and
// release gates apply. Install-only projects never show the selector.
function bimSOP(){ return !!(SOP && SOP.bimEnabled); }
function isEwp(){ return bimSOP() && pkgKind === 'ewp'; }
function setKind(k){
if(pkgKind === k) return;
pkgKind = (k === 'ewp') ? 'ewp' : 'iwp';
const tEl = document.getElementById('wp_type'); if(tEl) tEl.value = ''; // type list changes with kind
applyKind();
numberDirty = false; updateNumber(); updateReleaseBanner();
track('kind_changed', {kind: pkgKind});
}
function applyKind(){
const bimProj = bimSOP(), ewp = isEwp();
const show = (id, on) => { const el = document.getElementById(id); if(el) el.style.display = on ? '' : 'none'; };
show('bim-card', bim);
show('asset-card', !bim);
show('material-card', !bim);
show('mimo-card', !bim);
show('kind-row', bimProj);
show('bim-card', ewp); // LOD / model area / clash / scan
show('asset-card', !ewp); // controls.dev assets
show('material-card', !ewp); // bill of materials
show('mimo-card', !ewp); // kitting / MIMO
show('bimlink-wrap', bimProj && !ewp); // an IWP references the BIM package that enabled it
if(bimProj) setRadio('pkgkind', pkgKind);
buildTypePicker(); // filtered by kind
buildConstraints(); // filtered by kind
}
function buildCostCodes(){
const sel=document.getElementById('wp_cost'); const cur=sel.value;
@@ -183,7 +204,13 @@ function renderSpecFolderLink(){
const spec=sopLinkedSources().find(s=>/spec/i.test(s.label));
el.innerHTML = spec ? `<a href="${esc(spec.link)}" target="_blank" rel="noopener" class="ref-link-sm">↗ Open spec folder (${esc(spec.system||'SOP')})</a>` : '';
}
function buildTypePicker(){ document.getElementById('wp_type').innerHTML=`<option value="">Select…</option>`+enabledTypes().map(t=>`<option>${esc(t.name)}</option>`).join(''); }
function buildTypePicker(){
let types = enabledTypes();
if(bimSOP()) types = types.filter(t => isEwp() ? t.bim : !t.bim); // BIM types only for EWP, install types only for IWP
const sel=document.getElementById('wp_type'); const cur=sel.value;
sel.innerHTML=`<option value="">Select…</option>`+types.map(t=>`<option>${esc(t.name)}</option>`).join('');
if(types.some(t=>t.name===cur)) sel.value=cur;
}
function buildSequencePicker(){ const steps=((SOP&&SOP.sequence)||[]).filter(s=>s.kind!=='gate'&&(s.label||'').trim()); document.getElementById('wp_seq').innerHTML=`<option value="">None (no predecessor)</option>`+steps.map(s=>`<option>${esc(s.label)}</option>`).join(''); }
function onTypeChange(){
updateNumber(); track('type_selected');
@@ -689,6 +716,7 @@ function collectPackage(){
signoffs:pkgSignoffs.map(s=>({role:s.role,name:s.name,date:s.date,signed:s.signed,dateReason:s.dateReason||''})),
holds:pkgHolds.map(h=>({...h})),
actualHrs:gv('wp_actual_hrs'), installedQty:gv('wp_installed_qty'), redlines:gv('wp_redlines'), lessons:gv('wp_lessons'),
kind: pkgKind, // 'iwp' (install) | 'ewp' (BIM) — drives which fields/types/gates apply
// AWP traceability: which BIM/model package(s) enabled this install package.
bimlink:gv('wp_bimlink'),
// BIM/VDC package details (only meaningful on a BIM SOP).
@@ -712,8 +740,9 @@ function savePackage(view){
function renderPackage(pkg){
const r = pkg.constraints ? {open:pkg.constraints.filter(c=>c.status==='open').length,total:pkg.constraints.length} : {open:0,total:0};
const readyTxt = pkg.status==='Issue' ? 'ON HOLD' : (r.open===0?'RELEASE-READY':(r.open+' OPEN CONSTRAINTS'));
const kindLbl = pkg.kind==='ewp' ? 'BIM (EWP)' : 'IWP';
let h=`<h1>${esc(pkg.number||'(no number)')} — Work Package</h1>
<div class="doc-subtitle">${esc(pkg.project)} · TYPE: ${esc(pkg.type).toUpperCase()} · STATUS: ${esc(pkg.status).toUpperCase()} · ${readyTxt}</div>`;
<div class="doc-subtitle">${esc(pkg.project)} · ${kindLbl} · TYPE: ${esc(pkg.type).toUpperCase()} · STATUS: ${esc(pkg.status).toUpperCase()} · ${readyTxt}</div>`;
const costDesc = (COST_CODES.find(c=>c.split('|')[0]===pkg.cost)||'').split('|')[1];
// Project system links travel with the WP; fall back to the live SOP for older packages.
const plinks = (pkg.projectLinks&&pkg.projectLinks.length) ? pkg.projectLinks : ((SOP&&SOP.projectLinks)||[]);
@@ -875,6 +904,7 @@ function clearSaved(){ if(!savedPackages.length) return; if(!confirm('Delete all
function editPackage(i){ const p=savedPackages[i]; if(!p) return; editingId=p.id; loadPackageIntoForm(p); }
function loadExample(){ loadPackageIntoForm(JSON.parse(JSON.stringify(EXAMPLE_PKG))); editingId=null; toast('Example work package loaded'); track('example_loaded'); }
function loadPackageIntoForm(p){
pkgKind = (p.kind === 'ewp') ? 'ewp' : 'iwp'; // set before type/constraint pickers so they filter correctly
const set=(id,v)=>{const el=document.getElementById(id); if(el) el.value=v||'';};
set('wp_subject',p.subject); set('wp_system',p.system); set('wp_location',p.location);
set('wp_assignees',p.assignees); set('wp_distribution',p.distribution);
@@ -882,7 +912,7 @@ function loadPackageIntoForm(p){
set('wp_kit_owner',p.kitOwner); set('wp_kit_date',p.kitDate); set('wp_mimo_time',p.mimoTime); set('wp_mimo_loc',p.mimoLoc);
set('wp_actual_hrs',p.actualHrs); set('wp_installed_qty',p.installedQty); set('wp_redlines',p.redlines); set('wp_lessons',p.lessons);
set('wp_bimlink',p.bimlink); set('wp_lod',p.lod); set('wp_model_area',p.modelArea); set('wp_clash',p.clash); set('wp_scan_link',p.scanLink);
applyBimMode();
applyKind();
buildTypePicker(); document.getElementById('wp_type').value=p.type||'';
buildCostCodes(); document.getElementById('wp_cost').value=p.cost||'';
set('wp_wbs',p.wbs);
@@ -952,7 +982,7 @@ function newPackage(){
['wp_subject','wp_system','wp_location','wp_wbs','wp_assignees','wp_distribution','wp_due','wp_spec','wp_desc','wp_hours','wp_kit_owner','wp_kit_date','wp_mimo_time','wp_mimo_loc','wp_actual_hrs','wp_installed_qty','wp_redlines','wp_lessons','wp_bimlink','wp_model_area','wp_scan_link'].forEach(id=>{const el=document.getElementById(id); if(el) el.value='';});
document.getElementById('wp_type').value=''; document.getElementById('wp_kit_status').value=''; document.getElementById('wp_cost').value='';
['wp_lod','wp_clash'].forEach(id=>{const el=document.getElementById(id); if(el) el.value='';});
applyBimMode();
pkgKind='iwp'; applyKind();
setRadio('status','Draft');
numberDims={}; buildNumberDims();
pkgDisciplines=[]; pkgScope={}; pkgDiscStatus={}; buildDisciplinePicker(); renderScope(); onHoursChange();