Project Controls · Construction Claims · Dispute Advisory
Lopes Limited
Project Intelligence Engine
Modern Project Control
📐
Project Setup
Base Contract Setup
Excel (.xlsx) or CSV — the as-contracted WBS, pay items and budget (Hours/Labour/Material/Equipment) from tender. This becomes the baseline everything else measures against.
Do this once per project — everything else measures against this baseline
📅
Schedule Data
P6 Schedule
Drop a Primavera P6 XML export — including resource-loaded schedules — for instant schedule, delay and EVM analysis.
💰
Cost Data
Cost Control
Excel (.xlsx) or CSV — bring in budget, actuals and forecast alongside P6.
Optional — overrides P6 cost data where CBS codes match WBS
📋
Change Management
Change Order Log
Excel (.xlsx) or CSV — bring in your RFI/ECN log directly, independent of P6.
Optional — otherwise CO status is auto-tracked from your P6 RFI/ECN WBS
📝
Site Documentation
Daily Field Report
Open today's report template — manpower, equipment, work performed, safety and photo log. Fill in, save or print each day.
No P6 file needed — independent of the engine above
📈
Site Documentation
Weekly Progress Report
WBS/Activity list auto-fills from the loaded P6 schedule — fill in weekly % complete, work performed and issues.
Load a P6 file above first so the WBS/Activity list can auto-fill
📅 Schedule Analysis ⚠️ Delay Analysis 👷 Manhours & EVM 📋 Change Orders 🔴 RAG Health ⚡ No Install Needed

📝 Daily Field Report — WTG Site Documentation

Project Info
Site Conditions
Personnel
Manpower by Trade
Trade / CompanyWorkersHoursNotes
Equipment on Site
EquipmentQtyHours UsedIdle / Notes
Work Performed Today
Safety
Delays / Issues / Deficiencies
Photo Log
Photo Ref #Description / Location
This template logs photo references/descriptions only — attach the actual image files separately with your report.
Additional Notes

📈 Weekly Progress Report — WTG Site Documentation

Project Info
Progress by WBS (from P6 Schedule)
ID Activity BL Finish Previous Period % This Week % Total Progress % Status
Total Progress = Previous Period % + This Week % (capped at 100%). Status is set automatically: 0% = Not Started, 1–99% = In Progress, 100% = Complete.
Change Orders
CO / Ref No. Description WBS / Area Date Raised Status Cost Impact ($) Time Impact (days) Notes
Overall Summary

💰 Cost Data Template — Manual Entry

Project Info
Build a cost register by hand — no P6 or Excel file needed. Add a row per cost item, fill in Budget/Approved CO/Actual/Forecast, and CV calculates automatically. Save it as a template you can reopen and update anytime.
Cost Register
Description Category Original Budget Approved CO Actual Cost Forecast Final CV (auto)
Notes

📋 Change Order Log Template — Manual Entry

Project Info
Track RFIs and ECNs by hand — no P6 file needed. Add a row per change item; only ECNs marked Approved should be counted toward budget in your Cost Data.
Change Order Register
Ref Type Description Date Sent Status Man Hours Labor Cost Material Cost Equipment Cost Misc Cost Total Cost (auto) Notes

📐 Base Contract Setup — Manual Entry

Project Info
This is the as-contracted WBS and budget from tender/commercial close — build it once per project. Total Cost per row auto-calculates from Equipment + Material + Labour.
Base Contract WBS & Budget
WBS Code WBS Name Activity ID Activity Description Contract Model QtyUnitHours Equipment $Material $Labour $ Total (auto)
Lopes Limited |
Data Date:
💰 Cost
Load a P6 XML with cost data or enter costs in the Cost Data tab to see the cost S-curve.
📅 Schedule
📊 Progress
Activity Status
Progress by WBS
⚙️ Manhours & EVM
📋 Change Orders
WBS Progress
WBS Critical Path Map
Critical Path Activities
IDActivity NameWBSTFVariance Finish DateStatus
📐
HOW CHANGE ORDERS IMPACT SCHEDULE — 6 SCENARIOS
Reference guide for EOT analysis · Click to expand / collapse
1
CO Concurrent — No Delay to A
✅ NO EXTENSION
"Change ran alongside our work and closed on time — no impact"
A
A (Original Duration)
CO
CO
FF, Lag=0 — CO finishes with A, no extension
2
CO Concurrent — A Extended, No Lag
⭐ MOST COMMON
"The change pushed our finish — we couldn't complete until CO closed"
A
A (Original)
Extended
CO
CO
FF, Lag=0 — A finish = CO finish, A extended
3
CO Concurrent — A Extended with Lag
📋 KNOWN REMAINING DAYS
"CO closed, then we still needed X days to finish our remaining scope"
A
A (Original)
A Extended
CO
CO
+ Lag X days ──┘
FF + Lag = X days — A finishes X days after CO
4
CO Concurrent — FS, Unknown Remaining
⭐ ALSO VERY COMMON
"CO closed but we weren't sure how much work was left — updated each cycle"
A
A (Original)
???
CO
CO
FS, Lag=0 — remaining starts after CO, updated each cycle → upgrade to #3
5
CO as Predecessor — Late Start of A
⏱ A START DELAYED
"We couldn't start our work until the change was resolved first"
A
A (Original Duration)
CO
CO
FS — A can only start after CO fully finishes
6
Multi-Scope Impact
⚠️ HIGHEST RISK
"One change hit several crews working at the same time"
CO-001
FF↓FF↓FF↓
A
B
C
FS↓
D
🔑
KEY PRINCIPLE
The engine automatically detects which scenario applies to each CO by reading P6 predecessor/successor relationships. CO cost only flows into the current budget when status is Approved & In Progress. For unknown remaining scope use Scenario 4 (FS, no lag) and upgrade to Scenario 3 (FF + Lag) once the remaining days are confirmed.
📄 Section 1
Contract Activities
IDActivity NameWBS Actual StartActual Finish BL Finish %Variance Finish DateTFCPStatusFlag Delay Cause
📋 Section 2
Change Orders / RFI / ECN
IDActivity NameWBS / CO Group Actual StartActual Finish BL StartBL Finish %Variance Finish DateTFStatusType
🔍 FILTER RESOURCES
RESOURCE CATEGORY
STATUS
SEARCH RESOURCE
👷 By Resource Type
📄 Contract Resources
Resource Type
Category
Resource Name
From P6
Unit
hr/ton/unit
Rate
$/unit
Budget
Units
Actual
Units
Earned (EV)
BU×Act%
Remaining
Units
% Used
AC÷BU
Work Done
EV÷BU
CPI
EV÷AC
Status
CPI based
📋 Change Order Resources
CO Group
ECN/RFI/FCN
Resource Name
From P6
Unit
hr/ton/unit
Rate
$/unit
Budget
Units
Actual
Units
Earned (EV)
BU×Act%
Remaining
Units
% Used
AC÷BU
Work Done
EV÷BU
CPI
EV÷AC
Status
CPI based
📋 By Activity
📄 Contract Activities
Activity IDActivity NameWBSResourceType Budget UnitsActual Units Remaining Units% Used
📋 Change Order Activities
CO Group
ECN/RFI/FCN
Activity NameResourceType Budget UnitsActual Units Remaining Units% Used
🗂️ By WBS
📄 Contract WBS
WBS / Discipline Labour BudgetLabour Actual Equip BudgetEquip Actual Material BudgetMaterial Actual % Labour% Equip% Material
📋 Change Order WBS
CO Group
ECN/RFI/FCN
Labour BudgetLabour Actual Equip BudgetEquip Actual Material BudgetMaterial Actual % Labour% Equip% Material
💰 Cost Control
WBS Cost Summary
CategoryOriginal BudgetApproved COCurrent Budget (BAC) Earned Value (EV) CommittedActual to Date (AC)Forecast FinalCV = EV−AC% SpentHealth
Project Cost Register
Rows are pre-populated from your P6 WBS. Budget and Actual to Date auto-fill from P6 resource cost fields when available. Forecast Final auto-calculates as EAC (AC + Remaining÷CPI) — edit any field to override. Save to preserve data between sessions.
Cost Type Original BudgetApproved COCurrent Budget (BAC) % Complete (P6) Earned Value (EV) Committed Actual to Date (AC) Forecast Final (EAC) CV = EV−AC
📏 Method 1 — Measured Mile
The Measured Mile is the best-performing activity on the same project — the lowest hours-per-unit rate achieved by the same crew under normal, unimpacted conditions. This becomes the benchmark of what productivity should look like.

All other (impacted) activities are compared against this benchmark:
 • Impacted Rate = Actual Hours ÷ Units Installed (in disrupted period)
 • Best Rate (Measured Mile) = Actual Hours ÷ Units Installed (best unimpacted activity)
 • Lost Hours = (Impacted Rate − Best Rate) × Impacted Quantity
LoP Factor = 1 − (Best Rate ÷ Impacted Rate)  |  Lost Hours = (Impacted Rate − Best Rate) × Qty
📊 Method 2 — Earned Value
Compares manhours actually spent (ACWP) against the manhours that should have been spent for the work performed (BCWP/Earned). The gap represents lost manhours due to productivity disruption.
Lost MH = Actual MH − Earned MH  |  LoP% = (1 − CPI) × 100
🏆
The Measured Mile — Productivity Benchmark
Best performing activity — used as baseline for all comparisons
Measured Mile — Productivity Factor by Activity
Bar = productivity relative to measured mile (100% = no loss · below 100% = loss)
Lost Manhours by Activity (Actual vs Earned)
Red bars = manhours spent above what was earned (productivity loss quantum)
Loss of Productivity Register — All Activities
ID Activity Name WBS Budget MH Earned MH Actual MH Lost MH CPI LoP % (CPI) Productivity vs Mile LoP % (Mile) Severity
Change Order Register
RefTypeDescriptionCategory StartCloseStepsStatus ApprovedCO Cost ($)Linked To (Contract Activity)
'; var w2=window.open('','_blank','width=1400,height=950'); if(w2){w2.document.write(html);w2.document.close();} else{alert('Please allow popups to generate PDF.');} } function exportCurrentTabExcel(){ var tab = window._activeTab || 'cost'; if(typeof XLSX === 'undefined'){ var sc = document.createElement('script'); sc.src = 'https://cdnjs.cloudflare.com/ajax/libs/xlsx/0.18.5/xlsx.full.min.js'; sc.onload = function(){ doExcelExport(tab); }; document.head.appendChild(sc); } else { doExcelExport(tab); } } function doExcelExport(tab){ var wb = XLSX.utils.book_new(); var proj = (document.getElementById('proj-name')||{}).textContent || 'Project'; var dd = (document.getElementById('proj-dd')||{}).textContent || ''; // Which tables to export per tab var tableIds = { 'cost': ['cost-input-body','billing-body','evm-input-body','evm-full-body','map-body'], 'manhours': ['mh-body','mh-wbs-body'], 'resources': ['res-body'], 'schedule': ['sched-body','sched-co-body'], 'delay': ['delay-body','cp-body'], 'lop': ['lop-body'], 'changeorders':['co-body'], }; var tableNames = { 'cost-input-body':'Cost Data', 'billing-body':'Progress Billing', 'evm-input-body':'EVM Input', 'evm-full-body':'EVM Full Register', 'map-body':'CBS-WBS Mapping', 'mh-body':'Manhours by Activity', 'mh-wbs-body':'Manhours by WBS', 'res-body':'Resource Register', 'sched-body':'Contract Activities', 'sched-co-body':'Change Order Activities', 'delay-body':'Delay Register', 'cp-body':'Critical Path', 'lop-body':'LoP Register', 'co-body':'Change Order Log', }; var ids = tableIds[tab] || []; ids.forEach(function(id){ var tbody = document.getElementById(id); if(!tbody || !tbody.closest) return; var table = tbody.closest('table'); if(!table) return; // Clone table and strip input/select values into plain text cells var clone = table.cloneNode(true); clone.querySelectorAll('input,select').forEach(function(el){ var td = document.createElement('td'); td.textContent = el.value || el.getAttribute('placeholder') || ''; el.parentNode.replaceChild(td, el); }); clone.querySelectorAll('button').forEach(function(el){ el.remove(); }); var ws = XLSX.utils.table_to_sheet(clone, {raw:false}); XLSX.utils.book_append_sheet(wb, ws, (tableNames[id]||id).slice(0,31)); }); if(wb.SheetNames.length===0){ showToast('⚠️ Nothing to export on this tab'); return; } var fname = 'WTG_'+proj.replace(/[^a-z0-9]/gi,'_')+'_'+tab+'_'+dd+'.xlsx'; XLSX.writeFile(wb, fname); showToast('✅ Excel exported — '+fname); } function processFile(file){if(!file)return;if(!file.name.match(/\.(xml|xer)$/i)){showError('Please upload a P6 XML file');return;}hideError();var rd=new FileReader();rd.onload=function(e){try{var data=parseP6(e.target.result);if(!data.project.name)throw new Error('No project');uploadedP6Data=data;var st=document.getElementById('p6-status');if(st)st.innerHTML='✅ '+data.project.name+' — '+data.construction.length+' activities loaded';var dz=document.getElementById('drop-zone');if(dz){dz.classList.add('loaded');dz.classList.remove('next-step');}checkReadyToLaunch();}catch(err){showError('Could not parse this file. Please export as P6 XML from Primavera P6 Professional.');console.error(err);}};rd.readAsText(file);} function resetApp(){uploadedP6Data=null;uploadedCostData=null;costRows=[];mapRows=[];evmInputs={};billingRows=[];pendingCOBudgetGlobal=0;uploadedCOData=null;document.getElementById('upload-screen').style.display='flex';document.getElementById('dashboard').style.display='none';var fi=document.getElementById('file-input');if(fi)fi.value='';var cu=document.getElementById('cost-file-upload');if(cu)cu.value='';var lb=document.getElementById('launch-btn-wrap');if(lb)lb.style.display='none';var ps=document.getElementById('p6-status');if(ps)ps.innerHTML='';var cs=document.getElementById('cost-status');if(cs)cs.innerHTML='Optional — overrides P6 cost data';var cos=document.getElementById('co-status');if(cos)cos.innerHTML='Optional — otherwise CO status is auto-tracked from your P6 RFI/ECN WBS';var dz=document.getElementById('drop-zone');if(dz)dz.classList.remove('loaded');var cdz=document.getElementById('cost-drop-zone');if(cdz)cdz.classList.remove('loaded');var codz=document.getElementById('co-drop-zone');if(codz)codz.classList.remove('loaded');showTab('executive',document.querySelector('.tab'));} function showError(msg){var el=document.getElementById('error-box');if(el){el.textContent=msg;el.style.display='block';}} function hideError(){var el=document.getElementById('error-box');if(el)el.style.display='none';} // Attach event listeners directly - script is at end of body so DOM is ready (function(){ var fi=document.getElementById('file-input'); if(fi)fi.addEventListener('change',function(e){processFile(e.target.files[0]);}); var cu=document.getElementById('cost-file-upload'); if(cu)cu.addEventListener('change',function(e){processCostFile(e.target.files[0]);}); var dz=document.getElementById('drop-zone'); if(dz){ dz.addEventListener('dragover',function(e){e.preventDefault();dz.classList.add('dragging');}); dz.addEventListener('dragleave',function(){dz.classList.remove('dragging');}); dz.addEventListener('drop',function(e){e.preventDefault();dz.classList.remove('dragging');processFile(e.dataTransfer.files[0]);}); } var cdz=document.getElementById('cost-drop-zone'); if(cdz){ cdz.addEventListener('dragover',function(e){e.preventDefault();cdz.classList.add('dragging');}); cdz.addEventListener('dragleave',function(){cdz.classList.remove('dragging');}); cdz.addEventListener('drop',function(e){e.preventDefault();cdz.classList.remove('dragging');processCostFile(e.dataTransfer.files[0]);}); } })(); // ══ WEEKLY PROGRESS REPORT MODULE (welcome-page template) ══════════════════ // Same pattern as the Daily Field Report: a fillable weekly template opened // from the welcome page. WBS/Activity rows pre-fill from the currently // loaded P6 file (if one has been uploaded above) — same names, baseline // dates and P6 % Complete used everywhere else in the engine — so field // guys aren't retyping the WBS/activity list every week. Progress-this-week, // and change orders are free text/entry, filled in by hand. // // Table layout (v88): // ID | Activity | BL Finish | Previous Period % | This Week % | Total Progress % | Status // Rows are grouped under a WBS header band with a rolled-up % per group. // Total Progress = Previous Period + This Week (capped 100). Status derives // automatically from Total Progress: 0 = Not Started, 1-99 = In Progress, // 100 = Complete. function wprEsc(s){ return String(s==null?'':s).replace(/&/g,'&').replace(//g,'>').replace(/"/g,'"'); } // Status pill from a total-progress number function wprStatusOf(t){ if(t>=100) return 'Complete'; if(t<=0) return 'Not Started'; return 'In Progress'; } function wprStatusHtml(t){ const s=wprStatusOf(t); const cls = s==='Complete' ? 'wpr-st-cp' : (s==='Not Started' ? 'wpr-st-ns' : 'wpr-st-ip'); return ''+s+''; } // Group a flat activity list into WBS buckets, preserving a flat row index function wprBuildGroups(list){ const groups=[], map={}; list.forEach(function(a){ const key=(a.wbsName||'').trim()||'Unassigned WBS'; if(!map[key]){ map[key]={name:key, items:[]}; groups.push(map[key]); } map[key].items.push(a); }); groups.sort(function(x,y){ return x.name.localeCompare(y.name); }); groups.forEach(function(g){ g.items.sort(function(a,b){ const as=a.blStart?a.blStart.getTime():0, bs=b.blStart?b.blStart.getTime():0; if(as!==bs) return as-bs; return String(a.id||'').localeCompare(String(b.id||'')); }); }); return groups; } // Render the grouped table body. `list` items need: id, name, wbsName, // blFinishFmt/blFinish, pct (previous period %), and optionally weekPct. function wprRenderTable(list){ const rowsEl=document.getElementById('wpr-body'); if(!rowsEl) return; const groups=wprBuildGroups(list); let html='', idx=0; const ref=[], grpRef=[]; groups.forEach(function(g,gi){ const idxs=[]; html+=''+wprEsc(g.name)+ ''; g.items.forEach(function(a){ const i=idx++; idxs.push(i); const hasPrev=(a.pct!==undefined && a.pct!==null && a.pct!==''); const prev=hasPrev?(parseFloat(a.pct)||0):0; const wk=(a.weekPct!==undefined && a.weekPct!==null)?a.weekPct:''; const finish=(a.blFinishFmt || (typeof fmt==='function'?fmt(a.blFinish):'') || '—'); html+=''+ ''+wprEsc(a.id||'')+''+ ''+wprEsc(a.name||'')+''+ ''+wprEsc(finish)+''+ ''+(hasPrev?prev+'%':'—')+''+ ''+ '—'+ ''+ ''; ref.push({id:a.id, name:a.name, wbsName:g.name, prev:prev, hasPrev:hasPrev}); }); grpRef.push({name:g.name, idxs:idxs}); }); rowsEl.innerHTML=html; rowsEl.dataset.filled='1'; rowsEl.dataset.count=idx; window._wprActRef=ref; window._wprGroups=grpRef; wprRecalcAll(); } function wprRowTotal(i){ const prevEl=document.getElementById('wpr-prev-'+i); const prev=prevEl?(parseFloat(prevEl.dataset.prev)||0):0; const wkEl=document.getElementById('wpr-pct-'+i); const wk=wkEl?(parseFloat(wkEl.value)||0):0; let t=prev+wk; if(t>100) t=100; if(t<0) t=0; return Math.round(t*10)/10; } function wprRecalcRow(i){ const t=wprRowTotal(i); const tEl=document.getElementById('wpr-tot-'+i); if(tEl) tEl.textContent=t+'%'; const sEl=document.getElementById('wpr-stat-'+i); if(sEl) sEl.innerHTML=wprStatusHtml(t); wprRefreshGroupTotals(); } function wprRecalcAll(){ (window._wprActRef||[]).forEach(function(a,i){ const t=wprRowTotal(i); const tEl=document.getElementById('wpr-tot-'+i); if(tEl) tEl.textContent=t+'%'; const sEl=document.getElementById('wpr-stat-'+i); if(sEl) sEl.innerHTML=wprStatusHtml(t); }); wprRefreshGroupTotals(); } function wprRefreshGroupTotals(){ let all=0, allCnt=0, allPrev=0, allWk=0, dAll=0, pAll=0, nAll=0; (window._wprGroups||[]).forEach(function(g,gi){ let sum=0, cnt=0, done=0, prog=0; g.idxs.forEach(function(i){ const t=wprRowTotal(i); sum+=t; cnt++; if(t>=100) done++; else if(t>0) prog++; const pEl=document.getElementById('wpr-prev-'+i); allPrev += pEl?(parseFloat(pEl.dataset.prev)||0):0; const wEl=document.getElementById('wpr-pct-'+i); allWk += wEl?(parseFloat(wEl.value)||0):0; }); const avg=cnt?Math.round(sum/cnt*10)/10:0; const el=document.getElementById('wpr-grp-'+gi); if(el){ el.innerHTML=cnt+' activit'+(cnt===1?'y':'ies')+'  ·  '+done+' complete / '+prog+' in progress / '+ (cnt-done-prog)+' not started  ·  '+avg+'% total'; } all+=sum; allCnt+=cnt; dAll+=done; pAll+=prog; nAll+=(cnt-done-prog); }); const foot=document.getElementById('wpr-foot'); if(foot){ if(!allCnt){ foot.innerHTML=''; return; } const avgAll=Math.round(all/allCnt*10)/10; const avgPrev=Math.round(allPrev/allCnt*10)/10; const avgWk=Math.round(allWk/allCnt*10)/10; foot.innerHTML='PROJECT TOTAL — '+allCnt+' activit'+(allCnt===1?'y':'ies')+' in '+ (window._wprGroups||[]).length+' WBS groups'+ ''+avgPrev+'%'+avgWk+'%'+avgAll+'%'+ ''+dAll+' C / '+pAll+' IP / '+nAll+' NS'; } } // ── Change Orders (weekly report) ────────────────────────────────────────── let wprCORows=[], wprCOCounter=0; const WPR_CO_STATUSES=['Pending','Submitted','Approved','Rejected','Cancelled']; function wprCORowHtml(r,i){ const opts=WPR_CO_STATUSES.map(function(s){ return ''; }).join(''); return ''+ ''+ ''+ ''+ ''+ ''+ ''+ ''+ ''+ ''+ ''; } function wprCOAddRow(prefill){ const i=wprCOCounter++; const r=Object.assign({ref:'',desc:'',wbs:'',date:'',status:'Pending',cost:'',days:'',notes:''}, prefill||{}); wprCORows.push({idx:i, data:r}); const body=document.getElementById('wpr-co-body'); if(body) body.insertAdjacentHTML('beforeend', wprCORowHtml(r,i)); wprCORefreshTotals(); } function wprCORemoveRow(i){ const row=document.getElementById('wpr-co-row-'+i); if(row) row.remove(); wprCORows=wprCORows.filter(function(r){ return r.idx!==i; }); wprCORefreshTotals(); } function wprCOUpdate(i, field, val){ const r=wprCORows.find(function(r){ return r.idx===i; }); if(!r) return; r.data[field]=val; wprCORefreshTotals(); } function wprCORefreshTotals(){ const foot=document.getElementById('wpr-co-foot'); if(!foot) return; if(!wprCORows.length){ foot.innerHTML='No change orders logged — click “+ Add Change Order” to record one.'; return; } let cost=0, days=0, appCost=0, appDays=0, approved=0, pending=0; wprCORows.forEach(function(r){ const c=parseFloat(r.data.cost)||0, d=parseFloat(r.data.days)||0; cost+=c; days+=d; if(r.data.status==='Approved'){ approved++; appCost+=c; appDays+=d; } else if(r.data.status!=='Rejected' && r.data.status!=='Cancelled'){ pending++; } }); const money=function(v){ return (v<0?'-$':'$')+Math.abs(v).toLocaleString(undefined,{maximumFractionDigits:0}); }; foot.innerHTML=''+ ''+wprCORows.length+' change order'+(wprCORows.length===1?'':'s')+''+ ''+approved+' approved'+ ''+pending+' pending'+ ''+money(cost)+''+days+' d'+ 'Approved only: '+money(appCost)+' / '+appDays+' d'; } function openWeeklyProgressReport(){ const bannerEl=document.getElementById('wpr-p6-banner'); const rowsEl=document.getElementById('wpr-body'); if(uploadedP6Data && uploadedP6Data.construction && uploadedP6Data.construction.length){ if(bannerEl){ bannerEl.style.display='block'; bannerEl.className='wpr-banner wpr-banner-ok'; bannerEl.innerHTML='✅ Auto-filled from loaded P6 schedule: '+(uploadedP6Data.project.name||'Project')+' — '+uploadedP6Data.construction.length+' activities, grouped by WBS.'; } if(rowsEl && rowsEl.dataset.filled!=='1'){ const projNameEl=document.getElementById('wpr-project'); if(projNameEl && !projNameEl.value) projNameEl.value=uploadedP6Data.project.name||''; const jobEl=document.getElementById('wpr-jobno'); if(jobEl && !jobEl.value) jobEl.value=uploadedP6Data.project.id||''; wprRenderTable(uploadedP6Data.construction); } } else { if(bannerEl){ bannerEl.style.display='block'; bannerEl.className='wpr-banner wpr-banner-warn'; bannerEl.innerHTML='⚠️ No P6 file loaded yet — upload one above first so this template can auto-fill your WBS/Activity list. You can still fill in the report manually below.'; } if(rowsEl && rowsEl.children.length===0){ rowsEl.innerHTML='No activities loaded — upload a P6 file above, then reopen this template.'; } } wprCORefreshTotals(); const weekEl=document.getElementById('wpr-weekending'); if(weekEl && !weekEl.value) weekEl.value=new Date().toISOString().slice(0,10); document.getElementById('wpr-overlay').classList.add('open'); document.body.classList.add('dr-printing'); } function closeWeeklyProgressReport(){ document.getElementById('wpr-overlay').classList.remove('open'); document.body.classList.remove('dr-printing'); } function selectWeeklyProgressFile(){ openWeeklyProgressReport(); document.getElementById('wpr-file-input').click(); } function wprFieldIds(){ return ['wpr-project','wpr-jobno','wpr-weekending','wpr-preparedby', 'wpr-summary','wpr-safety','wpr-lookahead']; } function saveWeeklyProgressReport(){ const data={version:'WTG-WeeklyProgress-v2', savedAt:new Date().toISOString(), fields:{}, activityRows:[], changeOrders:[]}; wprFieldIds().forEach(function(id){ const el=document.getElementById(id); if(el) data.fields[id]=el.value; }); const ref=window._wprActRef||[]; ref.forEach(function(a,i){ const total=wprRowTotal(i); data.activityRows.push({ id:a.id, name:a.name, wbsName:a.wbsName, prevPct:a.hasPrev?a.prev:'', // Previous Period % (from P6 at time of save) pct:(document.getElementById('wpr-pct-'+i)||{}).value||'', // This Week % totalPct:total, // Total Progress % (prev + this week) status:wprStatusOf(total), }); }); wprCORows.forEach(function(r){ data.changeOrders.push(Object.assign({}, r.data)); }); const proj=(data.fields['wpr-project']||'Project').replace(/[^a-z0-9]+/gi,'_'); const week=data.fields['wpr-weekending']||new Date().toISOString().slice(0,10); const blob=new Blob([JSON.stringify(data,null,2)],{type:'application/json'}); const url=URL.createObjectURL(blob); const a=document.createElement('a'); a.href=url; a.download='WTG_WeeklyProgress_'+proj+'_'+week+'.json'; a.click(); URL.revokeObjectURL(url); showToast('✅ Weekly progress report saved — '+week); } function loadWeeklyProgressReport(){ document.getElementById('wpr-file-input').click(); } function onWeeklyProgressFileLoad(e){ const file=e.target.files[0]; if(!file) return; const reader=new FileReader(); reader.onload=function(ev){ try{ const obj=JSON.parse(ev.target.result); if(!obj.fields) throw new Error('Invalid file'); Object.keys(obj.fields).forEach(function(id){ const el=document.getElementById(id); if(el) el.value=obj.fields[id]; }); // If no P6 is currently loaded, rebuild the grouped table from the saved // file's own activity list so the report can still be reviewed/edited. if((!uploadedP6Data || !uploadedP6Data.construction) && obj.activityRows && obj.activityRows.length){ wprRenderTable(obj.activityRows.map(function(r){ return { id:r.id, name:r.name, wbsName:r.wbsName, blFinishFmt:'—', pct:(r.prevPct!==undefined && r.prevPct!=='')?r.prevPct:'', weekPct:r.pct||'' }; })); } else { // P6 is loaded: keep the live schedule rows, just restore This Week % // onto whichever rows match by activity ID (falls back to position). const ref=window._wprActRef||[]; (obj.activityRows||[]).forEach(function(r,i){ let target=-1; if(r.id){ target=ref.findIndex(function(a){ return String(a.id)===String(r.id); }); } if(target<0) target=i; const p=document.getElementById('wpr-pct-'+target); if(p) p.value=r.pct||''; }); } wprRecalcAll(); // Restore change orders wprCORows=[]; wprCOCounter=0; const coBody=document.getElementById('wpr-co-body'); if(coBody) coBody.innerHTML=''; (obj.changeOrders||[]).forEach(function(c){ wprCOAddRow(c); }); wprCORefreshTotals(); showToast('✅ Weekly progress report loaded — '+(obj.fields['wpr-weekending']||'')); } catch(err){ showToast('❌ Could not read file — please use a WTG Weekly Progress Report file'); } }; reader.readAsText(file); e.target.value=''; } function clearWeeklyProgressReport(){ if(!confirm('Clear all fields on this weekly report? This cannot be undone.')) return; wprFieldIds().forEach(function(id){ const el=document.getElementById(id); if(el && id!=='wpr-project' && id!=='wpr-jobno') el.value=''; }); (window._wprActRef||[]).forEach(function(a,i){ const p=document.getElementById('wpr-pct-'+i); if(p) p.value=''; }); wprRecalcAll(); wprCORows=[]; wprCOCounter=0; const coBody=document.getElementById('wpr-co-body'); if(coBody) coBody.innerHTML=''; wprCORefreshTotals(); document.getElementById('wpr-weekending').value=new Date().toISOString().slice(0,10); } // ══ MANUAL COST TEMPLATE MODULE (welcome-page template) ═════════════════════ // Same pattern as the Daily/Weekly templates: a standalone fillable cost // register, independent of any P6 or Excel/CSV upload. Useful for building a // cost register by hand, or for a project where no P6/cost file exists yet. var ctRows = []; var ctRowCounter = 0; function ctRowTemplate(r, i){ const cats = ["Labour","Equipment","Material","Subcontract","Engineering","Indirect / Overhead","Contingency","Other"]; const opts = cats.map(function(c){ return ''; }).join(''); return ''+ ''+ ''+ ''+ ''+ ''+ ''+ ''+ ''; } function ctAddRow(){ const i = ctRowCounter++; const r = {desc:'',category:'Labour',budget:'',approvedCO:'',actual:'',forecast:''}; ctRows.push({idx:i, data:r}); const body = document.getElementById('ct-body'); if(body) body.insertAdjacentHTML('beforeend', ctRowTemplate(r, i)); ctRefreshRow(i); } function ctRemoveRow(i){ const row = document.getElementById('ct-row-'+i); if(row) row.remove(); ctRows = ctRows.filter(function(r){ return r.idx!==i; }); ctRefreshTotals(); } function ctUpdate(i, field, val){ const r = ctRows.find(function(r){ return r.idx===i; }); if(!r) return; r.data[field] = val; ctRefreshRow(i); } function ctRefreshRow(i){ const r = ctRows.find(function(r){ return r.idx===i; }); if(!r) return; const budget = (parseFloat(r.data.budget)||0) + (parseFloat(r.data.approvedCO)||0); const actual = parseFloat(r.data.actual)||0; const cv = budget - actual; // simple CV: current budget minus actual, since no % complete tracked manually here const cvEl = document.getElementById('ct-cv-'+i); if(cvEl){ cvEl.textContent = (cv>=0?'+':'') + cv.toLocaleString(undefined,{maximumFractionDigits:0}); cvEl.style.color = cv>=0 ? '#1E8449' : '#B03A2E'; } ctRefreshTotals(); } function ctRefreshTotals(){ const foot = document.getElementById('ct-foot'); if(!foot) return; let totBudget=0, totCO=0, totActual=0, totForecast=0; ctRows.forEach(function(r){ totBudget += parseFloat(r.data.budget)||0; totCO += parseFloat(r.data.approvedCO)||0; totActual += parseFloat(r.data.actual)||0; totForecast += parseFloat(r.data.forecast)||0; }); const totCV = (totBudget+totCO) - totActual; const fmt = function(n){ return n.toLocaleString(undefined,{maximumFractionDigits:0}); }; foot.innerHTML = ''+ 'TOTAL'+ ''+fmt(totBudget)+''+ ''+fmt(totCO)+''+ ''+fmt(totActual)+''+ ''+fmt(totForecast)+''+ ''+(totCV>=0?'+':'')+fmt(totCV)+''+ ''; } function openCostTemplate(){ const dateEl = document.getElementById('ct-date'); if(dateEl && !dateEl.value) dateEl.value = new Date().toISOString().slice(0,10); if(ctRows.length===0) ctAddRow(); document.getElementById('ct-overlay').classList.add('open'); document.body.classList.add('dr-printing'); } function closeCostTemplate(){ document.getElementById('ct-overlay').classList.remove('open'); document.body.classList.remove('dr-printing'); } function ctFieldIds(){ return ['ct-project','ct-jobno','ct-date','ct-notes']; } function saveCostTemplate(){ const data = {version:'WTG-CostTemplate-v1', savedAt:new Date().toISOString(), fields:{}, rows:[]}; ctFieldIds().forEach(function(id){ const el = document.getElementById(id); if(el) data.fields[id] = el.value; }); ctRows.forEach(function(r){ data.rows.push(r.data); }); const proj = (data.fields['ct-project']||'Project').replace(/[^a-z0-9]+/gi,'_'); const date = data.fields['ct-date']||new Date().toISOString().slice(0,10); const blob = new Blob([JSON.stringify(data,null,2)], {type:'application/json'}); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = 'WTG_CostTemplate_'+proj+'_'+date+'.json'; a.click(); URL.revokeObjectURL(url); showToast('✅ Cost template saved — '+date); } function loadCostTemplate(){ document.getElementById('ct-file-input').click(); } function onCostTemplateFileLoad(e){ const file = e.target.files[0]; if(!file) return; const reader = new FileReader(); reader.onload = function(ev){ try{ const obj = JSON.parse(ev.target.result); if(!obj.fields) throw new Error('Invalid file'); Object.keys(obj.fields).forEach(function(id){ const el = document.getElementById(id); if(el) el.value = obj.fields[id]; }); const body = document.getElementById('ct-body'); body.innerHTML = ''; ctRows = []; ctRowCounter = 0; (obj.rows||[]).forEach(function(rowData){ const i = ctRowCounter++; ctRows.push({idx:i, data:rowData}); body.insertAdjacentHTML('beforeend', ctRowTemplate(rowData, i)); ctRefreshRow(i); }); if(ctRows.length===0) ctAddRow(); showToast('✅ Cost template loaded — '+(obj.fields['ct-date']||'')); } catch(err){ showToast('❌ Could not read file — please use a WTG Cost Template file'); } }; reader.readAsText(file); e.target.value = ''; } function clearCostTemplate(){ if(!confirm('Clear this cost template? This cannot be undone.')) return; const body = document.getElementById('ct-body'); if(body) body.innerHTML = ''; ctRows = []; ctRowCounter = 0; ctAddRow(); const notesEl = document.getElementById('ct-notes'); if(notesEl) notesEl.value = ''; document.getElementById('ct-date').value = new Date().toISOString().slice(0,10); } // ══ CHANGE ORDER LOG MODULE (welcome-page template + file upload) ═══════════ // Same pattern as the Cost Template: a standalone RFI/ECN register, usable // either by uploading a simple Excel/CSV file or by building the log by hand. // Independent of P6 — separate from the auto-tracked CO status derived from // the P6 RFI/ECN WBS inside the engine itself. var colRows = []; var colRowCounter = 0; var uploadedCOData = null; const COL_STATUSES = ["Open","In Progress","Approved & In Progress","Not Approved","Closed"]; const COL_TYPES = ["RFI","ECN","FCN","LME","SI","FWI"]; function colRowTemplate(r, i){ const typeOpts = COL_TYPES.map(function(t){ return ''; }).join(''); const statusOpts = COL_STATUSES.map(function(s){ return ''; }).join(''); return ''+ ''+ ''+ ''+ ''+ ''+ ''+ ''+ ''+ ''+ ''+ ''+ ''+ ''; } function colRowTotalCost(r){ return (parseFloat(r.laborCost)||0) + (parseFloat(r.materialCost)||0) + (parseFloat(r.equipmentCost)||0) + (parseFloat(r.miscCost)||0); } function colAddRow(prefill){ const i = colRowCounter++; const r = Object.assign({ref:'',type:'ECN',desc:'',dateSent:'',status:'Open', manHours:'',laborCost:'',materialCost:'',equipmentCost:'',miscCost:'',notes:''}, prefill||{}); colRows.push({idx:i, data:r}); const body = document.getElementById('col-body'); if(body) body.insertAdjacentHTML('beforeend', colRowTemplate(r, i)); colRefreshRow(i); } function colRemoveRow(i){ const row = document.getElementById('col-row-'+i); if(row) row.remove(); colRows = colRows.filter(function(r){ return r.idx!==i; }); colRefreshTotals(); } function colUpdate(i, field, val){ const r = colRows.find(function(r){ return r.idx===i; }); if(!r) return; r.data[field] = val; colRefreshRow(i); } function colRefreshRow(i){ const r = colRows.find(function(r){ return r.idx===i; }); if(!r) return; const totalEl = document.getElementById('col-total-'+i); if(totalEl){ const t = colRowTotalCost(r.data); totalEl.textContent = '$'+t.toLocaleString(undefined,{maximumFractionDigits:0}); } colRefreshTotals(); } function colRefreshTotals(){ const foot = document.getElementById('col-foot'); if(!foot) return; let total=colRows.length, approved=0, notApproved=0, open=0, closed=0; let totalMH=0, totalLabor=0, totalMaterial=0, totalEquipment=0, totalMisc=0, totalCost=0, approvedCost=0; colRows.forEach(function(r){ const c = colRowTotalCost(r.data); totalMH += parseFloat(r.data.manHours)||0; totalLabor += parseFloat(r.data.laborCost)||0; totalMaterial += parseFloat(r.data.materialCost)||0; totalEquipment += parseFloat(r.data.equipmentCost)||0; totalMisc += parseFloat(r.data.miscCost)||0; totalCost += c; if(r.data.status==='Approved & In Progress'){ approved++; approvedCost+=c; } else if(r.data.status==='Not Approved') notApproved++; else if(r.data.status==='Open'||r.data.status==='In Progress') open++; else if(r.data.status==='Closed') closed++; }); const fmt = function(n){ return n.toLocaleString(undefined,{maximumFractionDigits:0}); }; foot.innerHTML = ''+ 'TOTAL: '+total+' | Approved: '+approved+' | Not Approved: '+notApproved+' | Open: '+open+' | Closed: '+closed+''+ ''+totalMH.toLocaleString()+''+ '$'+fmt(totalLabor)+''+ '$'+fmt(totalMaterial)+''+ '$'+fmt(totalEquipment)+''+ '$'+fmt(totalMisc)+''+ '$'+fmt(totalCost)+''+ 'Approved $'+fmt(approvedCost)+''; } function openCOTemplate(){ const dateEl = document.getElementById('col-date'); if(dateEl && !dateEl.value) dateEl.value = new Date().toISOString().slice(0,10); if(colRows.length===0) colAddRow(); document.getElementById('col-overlay').classList.add('open'); document.body.classList.add('dr-printing'); } function closeCOTemplate(){ document.getElementById('col-overlay').classList.remove('open'); document.body.classList.remove('dr-printing'); } function colFieldIds(){ return ['col-project','col-jobno','col-date']; } function saveCOTemplate(){ const data = {version:'WTG-COLog-v1', savedAt:new Date().toISOString(), fields:{}, rows:[]}; colFieldIds().forEach(function(id){ const el = document.getElementById(id); if(el) data.fields[id] = el.value; }); colRows.forEach(function(r){ data.rows.push(r.data); }); const proj = (data.fields['col-project']||'Project').replace(/[^a-z0-9]+/gi,'_'); const date = data.fields['col-date']||new Date().toISOString().slice(0,10); const blob = new Blob([JSON.stringify(data,null,2)], {type:'application/json'}); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = 'WTG_ChangeOrderLog_'+proj+'_'+date+'.json'; a.click(); URL.revokeObjectURL(url); showToast('✅ Change order log saved — '+date); } function loadCOTemplate(){ document.getElementById('col-file-input').click(); } function colRebuildTable(rows){ const body = document.getElementById('col-body'); body.innerHTML = ''; colRows = []; colRowCounter = 0; (rows||[]).forEach(function(rowData){ const i = colRowCounter++; colRows.push({idx:i, data:rowData}); body.insertAdjacentHTML('beforeend', colRowTemplate(rowData, i)); }); if(colRows.length===0) colAddRow(); colRows.forEach(function(r){ colRefreshRow(r.idx); }); colRefreshTotals(); } function onCOTemplateFileLoad(e){ const file = e.target.files[0]; if(!file) return; const reader = new FileReader(); reader.onload = function(ev){ try{ const obj = JSON.parse(ev.target.result); if(!obj.fields) throw new Error('Invalid file'); Object.keys(obj.fields).forEach(function(id){ const el = document.getElementById(id); if(el) el.value = obj.fields[id]; }); colRebuildTable(obj.rows); showToast('✅ Change order log loaded — '+(obj.fields['col-date']||'')); } catch(err){ showToast('❌ Could not read file — please use a WTG Change Order Log file'); } }; reader.readAsText(file); e.target.value = ''; } function clearCOTemplate(){ if(!confirm('Clear this change order log? This cannot be undone.')) return; colRebuildTable([]); document.getElementById('col-date').value = new Date().toISOString().slice(0,10); } // ── File upload path (Select CO File box on welcome page) ── // Parses a simple Excel/CSV export into the same row structure used by the // manual template above, so uploaded and hand-built logs behave identically. function buildCORowsFromUpload(rawRows){ return rawRows.map(function(row){ const get = function(){ const wanted = Array.prototype.slice.call(arguments); for(var i=0;i=0; }); return found || 'ECN'; })(), desc: get('description','desc')||'', dateSent: get('date sent','datesent','date')||'', status: status, manHours: get('man hours','manhours','hours')||'', laborCost: get('labor cost','labour cost','labor')||'', materialCost: get('material cost','material')||'', equipmentCost: get('equipment cost','equipment')||'', miscCost: get('misc cost','misc','miscellaneous')||get('cost','cost ($)','amount')||'', notes: get('notes','note')||'', }; }); } function parseCSVCO(text){ const lines = text.split(/\r?\n/).filter(function(l){ return l.trim()!==''; }); if(lines.length<2) return []; const headers = lines[0].split(',').map(function(h){ return h.trim().toLowerCase(); }); return lines.slice(1).map(function(line){ const cells = line.split(','); const row = {}; headers.forEach(function(h,i){ row[h] = (cells[i]||'').trim(); }); return row; }); } function processCOFile(file){ if(!file) return; const ext = file.name.split('.').pop().toLowerCase(); const st = document.getElementById('co-status'); if(ext==='csv'){ const rd = new FileReader(); rd.onload = function(e){ try{ uploadedCOData = buildCORowsFromUpload(parseCSVCO(e.target.result)); colRebuildTable(uploadedCOData); if(st) st.innerHTML = '✅ '+uploadedCOData.length+' change orders loaded'; const cdz = document.getElementById('co-drop-zone'); if(cdz) cdz.classList.add('loaded'); } catch(e2){ if(st) st.innerHTML = '❌ Could not parse CSV'; } }; rd.readAsText(file); } else if(ext==='xlsx'){ if(typeof XLSX==='undefined'){ const sc = document.createElement('script'); sc.src = 'https://cdnjs.cloudflare.com/ajax/libs/xlsx/0.18.5/xlsx.full.min.js'; sc.onload = function(){ processCOXLSX(file); }; document.head.appendChild(sc); } else processCOXLSX(file); } else if(st){ st.innerHTML = '❌ Please upload .xlsx or .csv only'; } } function processCOXLSX(file){ const rd = new FileReader(); rd.onload = function(e){ try{ const wb = XLSX.read(e.target.result, {type:'array'}); const sn = wb.SheetNames.indexOf('CHANGE ORDERS')>=0 ? 'CHANGE ORDERS' : wb.SheetNames[0]; const ws = wb.Sheets[sn]; const rows = XLSX.utils.sheet_to_json(ws, {defval:''}); uploadedCOData = buildCORowsFromUpload(rows); colRebuildTable(uploadedCOData); const st = document.getElementById('co-status'); if(st) st.innerHTML = '✅ '+uploadedCOData.length+' change orders from Excel'; const cdz = document.getElementById('co-drop-zone'); if(cdz) cdz.classList.add('loaded'); } catch(err){ const st = document.getElementById('co-status'); if(st) st.innerHTML = '❌ Cannot read Excel'; console.error(err); } }; rd.readAsArrayBuffer(file); } // ══ BASE CONTRACT SETUP MODULE (welcome-page template + file upload) ════════ // The as-contracted WBS/budget baseline for a project — set up ONCE, either // by uploading the tender/commercial cost breakdown, or building it by hand. // This is intentionally NOT yet wired into distribution logic across the // other boxes (Cost Data, CO, Progress Billing) — that mapping comes next. var bcRows = []; var bcRowCounter = 0; var uploadedBaseContractData = null; // ── Base Contract manual template: grouped structure ──────────────────────── // bcGroups = [{wbsCode, wbsName, acts:[{idx, data:{actId,actDesc,contractModel, // qty,unit,hours,equipmentCost,materialCost,labourCost}}]}] var bcGroups = []; var bcRows = []; // kept for backward-compat with save/load/upload code var bcRowCounter = 0; var uploadedBaseContractData = null; function bcActTemplate(gi, ai, act){ const i = act.idx; const d = act.data; return ''+ ''+ ''+ ''+ ''+ ''+ ''+ ''+ ''+ ''+ ''+ ''+ ''+ ''; } function bcGroupHTML(gi, g){ let html = ''+ ''+ ''+ ''+g.acts.length+' activit'+(g.acts.length===1?'y':'ies')+''+ ''+ ''+ ''; g.acts.forEach(function(act,ai){ html += bcActTemplate(gi, ai, act); }); html += ''+ ''+ ''; return html; } function bcRebuildUI(){ const body = document.getElementById('bc-body'); if(!body) return; let html = ''; bcGroups.forEach(function(g,gi){ html += bcGroupHTML(gi,g); }); body.innerHTML = html; bcGroups.forEach(function(g,gi){ g.acts.forEach(function(act,ai){ bcRefreshActTotal(gi,ai); }); }); bcRefreshTotals(); } function bcAddWBSGroup(){ bcGroups.push({wbsCode:'', wbsName:'', acts:[]}); const gi = bcGroups.length-1; bcAddActivity(gi, true); // add one blank activity automatically } function bcAddActivity(gi, silent){ if(!bcGroups[gi]) return; const act = {idx: bcRowCounter++, data:{actId:'',actDesc:'',contractModel:'Lump Sum',qty:1,unit:'Lot',hours:'',equipmentCost:'',materialCost:'',labourCost:''}}; bcGroups[gi].acts.push(act); bcRebuildUI(); if(!silent){ setTimeout(function(){ const body = document.getElementById('bc-body'); const lastInput = body && body.querySelector('#bc-addact-'+gi); if(lastInput) lastInput.scrollIntoView({behavior:'smooth', block:'nearest'}); },80); } } function bcRemoveGroup(gi){ if(!confirm('Remove this WBS group and all its activities?')) return; bcGroups.splice(gi,1); bcRebuildUI(); } function bcRemoveAct(gi, ai){ if(!bcGroups[gi]) return; bcGroups[gi].acts.splice(ai,1); bcRebuildUI(); } function bcGroupUpdate(gi, field, val){ if(!bcGroups[gi]) return; bcGroups[gi][field] = val; // Update the activity-count cell const grpRow = document.getElementById('bc-group-'+gi); if(grpRow){ const cnt = grpRow.querySelectorAll('td')[2]; if(cnt) cnt.textContent = bcGroups[gi].acts.length+' activit'+(bcGroups[gi].acts.length===1?'y':'ies'); } } function bcActUpdate(gi, ai, field, val){ if(!bcGroups[gi] || !bcGroups[gi].acts[ai]) return; bcGroups[gi].acts[ai].data[field] = val; bcRefreshActTotal(gi,ai); } function bcActTotalCost(d){ return (parseFloat(d.equipmentCost)||0)+(parseFloat(d.materialCost)||0)+(parseFloat(d.labourCost)||0); } function bcRefreshActTotal(gi,ai){ const el = document.getElementById('bc-total-'+gi+'-'+ai); if(!el || !bcGroups[gi] || !bcGroups[gi].acts[ai]) return; const t = bcActTotalCost(bcGroups[gi].acts[ai].data); el.textContent = t>0 ? '$'+t.toLocaleString(undefined,{maximumFractionDigits:0}) : '—'; bcRefreshTotals(); } function bcRefreshTotals(){ const foot = document.getElementById('bc-foot'); if(!foot) return; let totalHours=0, totalE=0, totalM=0, totalL=0, totalC=0, totalActs=0; bcGroups.forEach(function(g){ g.acts.forEach(function(act){ const d=act.data; totalHours += parseFloat(d.hours)||0; totalE += parseFloat(d.equipmentCost)||0; totalM += parseFloat(d.materialCost)||0; totalL += parseFloat(d.labourCost)||0; totalC += bcActTotalCost(d); totalActs++; }); }); const fmt=function(n){ return n.toLocaleString(undefined,{maximumFractionDigits:0}); }; foot.innerHTML=''+ ''+bcGroups.length+' WBS Groups'+ ''+totalActs+' Activities Total'+ ''+fmt(totalHours)+''+ '$'+fmt(totalE)+''+ '$'+fmt(totalM)+''+ '$'+fmt(totalL)+''+ '$'+fmt(totalC)+''+ ''; } // ── Compatibility: flatten bcGroups → bcRows for save/upload path ──────────── function bcSyncRows(){ bcRows = []; bcGroups.forEach(function(g){ g.acts.forEach(function(act){ bcRows.push({idx:act.idx, data:Object.assign({wbsCode:g.wbsCode, desc:act.data.actDesc, p6MatchKey:act.data.actDesc, item:act.data.actId}, act.data)}); }); }); } function openBaseContract(){ if(bcGroups.length===0) bcAddWBSGroup(); document.getElementById('bc-overlay').classList.add('open'); document.body.classList.add('dr-printing'); } function closeBaseContract(){ bcSyncRows(); // sync before closing so cost data can use bcRows document.getElementById('bc-overlay').classList.remove('open'); document.body.classList.remove('dr-printing'); } function bcFieldIds(){ return ['bc-project','bc-projno','bc-pkgno','bc-contractor','bc-revision','bc-date']; } function saveBaseContract(){ bcSyncRows(); const data={version:'WTG-BaseContract-v2',savedAt:new Date().toISOString(),fields:{},groups:bcGroups,rows:bcRows}; bcFieldIds().forEach(function(id){ const el=document.getElementById(id); if(el) data.fields[id]=el.value; }); const proj=(data.fields['bc-project']||'Project').replace(/[^a-z0-9]+/gi,'_'); const blob=new Blob([JSON.stringify(data,null,2)],{type:'application/json'}); const url=URL.createObjectURL(blob); const a=document.createElement('a'); a.href=url; a.download='WTG_BaseContract_'+proj+'.json'; a.click(); URL.revokeObjectURL(url); showToast('✅ Base contract setup saved'); } function loadBaseContract(){ document.getElementById('bc-file-input').click(); } function bcRebuildTable(rows){ // Convert flat rows array (from file upload) into grouped structure // Group by wbsCode; use description as activity desc const groupMap = {}; const groupOrder = []; bcRowCounter = 0; (rows||[]).forEach(function(d){ const gKey = (d.wbsCode||d.item||'WBS-1').trim(); if(!groupMap[gKey]){ groupMap[gKey]={wbsCode:gKey, wbsName:d.wbsCode||gKey, acts:[]}; groupOrder.push(gKey); } groupMap[gKey].acts.push({idx:bcRowCounter++, data:{ actId: d.item||'', actDesc: d.desc||d.p6MatchKey||'', contractModel: d.contractModel||'Lump Sum', qty: d.qty||1, unit: d.unit||'Lot', hours: d.hours||'', equipmentCost: d.equipmentCost||'', materialCost: d.materialCost||'', labourCost: d.labourCost||'', }}); }); bcGroups = groupOrder.map(function(k){ return groupMap[k]; }); if(bcGroups.length===0) bcAddWBSGroup(); else bcRebuildUI(); bcSyncRows(); } function onBaseContractFileLoad(e){ const file=e.target.files[0]; if(!file) return; const reader=new FileReader(); reader.onload=function(ev){ try{ const obj=JSON.parse(ev.target.result); if(!obj.fields) throw new Error('Invalid'); Object.keys(obj.fields).forEach(function(id){ const el=document.getElementById(id); if(el) el.value=obj.fields[id]; }); if(obj.groups && obj.groups.length>0){ bcGroups=obj.groups; bcRebuildUI(); bcSyncRows(); } else if(obj.rows){ bcRebuildTable(obj.rows); } showToast('✅ Base contract setup loaded'); } catch(err){ showToast('❌ Could not read file — please use a WTG Base Contract file'); } }; reader.readAsText(file); e.target.value=''; } function clearBaseContract(){ if(!confirm('Clear this base contract setup? This cannot be undone.')) return; bcGroups=[]; bcRows=[]; bcRebuildUI(); bcAddWBSGroup(); } // Stubs kept for backward compat — now unused but referenced by old bcRebuildTable path above function bcRowTotalCost(r){ return bcActTotalCost(r); } function bcAddRow(prefill){ bcAddActivity(bcGroups.length-1); } function bcRemoveRow(i){ /* handled by bcRemoveAct */ } function bcUpdate(i,f,v){ /* handled by bcActUpdate */ } function bcRefreshRow(i){ /* handled by bcRefreshActTotal */ } // ── File upload path (Select Contract File box on welcome page) ── function buildBaseContractRowsFromUpload(rawRows){ return rawRows.map(function(row){ // Strip out ($), (), * from key names and match by substring (contains) not exact // so "Equipment ($)" matches "equipment", "Labour ($)" matches "labour", etc. const get = function(){ const wanted = Array.prototype.slice.call(arguments); for(var i=0;i=0 || wanted[i].indexOf(clean)>=0; }); if(k !== undefined && row[k]!=='') return row[k]; } return ''; }; const desc = get('description','desc')||''; const item = get('item')||''; return { item: item, // Use Description as WBS name — this is what P6 activity names are based on // (e.g. "Mobilization & Site Setup"), not the code "A1000" which P6 doesn't know wbsCode: get('wbs code','wbs')||item, desc: desc, payItemCode: get('pay item code','pay item')||'', contractModel: get('contract model')||'Lump Sum', qty: get('total qty','qty','quantity')||'', unit: get('unit of measure','unit')||'Lot', hours: get('hours')||'', equipmentCost: get('equipment')||'', materialCost: get('material')||'', labourCost: get('labour','labor')||'', // Keep description as the WBS match key for P6 matching p6MatchKey: desc || item, }; }).filter(function(r){ if(!r.item && !r.desc) return false; // Skip totals rows, blank rows, and note rows const skipWords = ['total','note:','project total','sub total','subtotal']; const combined = ((r.item||'')+(r.desc||'')).toLowerCase().trim(); if(!combined) return false; if(skipWords.some(function(w){ return combined.indexOf(w)>=0; })) return false; return true; }); } function parseCSVBaseContract(text){ const lines = text.split(/\r?\n/).filter(function(l){ return l.trim()!==''; }); if(lines.length<2) return []; const headers = lines[0].split(',').map(function(h){ return h.trim().toLowerCase(); }); return lines.slice(1).map(function(line){ const cells = line.split(','); const row = {}; headers.forEach(function(h,i){ row[h] = (cells[i]||'').trim(); }); return row; }); } function processBaseContractFile(file){ if(!file) return; const ext = file.name.split('.').pop().toLowerCase(); const st = document.getElementById('bc-status'); if(ext==='csv'){ const rd = new FileReader(); rd.onload = function(e){ try{ uploadedBaseContractData = buildBaseContractRowsFromUpload(parseCSVBaseContract(e.target.result)); bcRebuildTable(uploadedBaseContractData); if(st) st.innerHTML = '✅ '+uploadedBaseContractData.length+' WBS items loaded  →  Now load your P6 Schedule above to launch the engine ↑'; const bdz = document.getElementById('bc-drop-zone'); if(bdz) bdz.classList.add('loaded'); if(!uploadedP6Data){ const p6dz = document.getElementById('drop-zone'); if(p6dz) p6dz.classList.add('next-step'); } } catch(e2){ if(st) st.innerHTML = '❌ Could not parse CSV'; } }; rd.readAsText(file); } else if(ext==='xlsx'){ if(typeof XLSX==='undefined'){ const sc = document.createElement('script'); sc.src = 'https://cdnjs.cloudflare.com/ajax/libs/xlsx/0.18.5/xlsx.full.min.js'; sc.onload = function(){ processBaseContractXLSX(file); }; document.head.appendChild(sc); } else processBaseContractXLSX(file); } else if(st){ st.innerHTML = '❌ Please upload .xlsx or .csv only'; } } function processBaseContractXLSX(file){ const rd = new FileReader(); rd.onload = function(e){ try{ const wb = XLSX.read(e.target.result, {type:'array'}); const sn = wb.SheetNames.indexOf('BASE CONTRACT')>=0 ? 'BASE CONTRACT' : wb.SheetNames[0]; const ws = wb.Sheets[sn]; // Scan for the real header row by looking for a row containing known column names. // The WTG-generated template has 3 info rows above the actual headers — but any // multi-row layout (user's own template) works the same way. const range = XLSX.utils.decode_range(ws['!ref'] || 'A1'); const knownHeaders = ['item','wbs','description','hours','labour','labor','equipment','material']; let headerRow = 0; // 0-based row index, default = first row for(let r = range.s.r; r <= Math.min(range.e.r, 9); r++){ let matchCount = 0; for(let c = range.s.c; c <= range.e.c; c++){ const cell = ws[XLSX.utils.encode_cell({r,c})]; if(cell && cell.v && knownHeaders.some(h => String(cell.v).toLowerCase().trim().indexOf(h)>=0)){ matchCount++; } } if(matchCount >= 2){ headerRow = r; break; } } const rows = XLSX.utils.sheet_to_json(ws, {defval:'', range:headerRow}); uploadedBaseContractData = buildBaseContractRowsFromUpload(rows); bcRebuildTable(uploadedBaseContractData); const st = document.getElementById('bc-status'); const count = uploadedBaseContractData.length; if(st) st.innerHTML = count > 0 ? '✅ '+count+' WBS items loaded  →  Now load your P6 Schedule above to launch the engine ↑' : '⚠️ File read OK but 0 rows matched — check column headers include: Item, WBS, Description, Hours, Labour, Material, Equipment'; const bdz = document.getElementById('bc-drop-zone'); if(bdz && count>0){ bdz.classList.add('loaded'); // Pulse the P6 box so user sees clearly what to do next if(!uploadedP6Data){ const p6dz = document.getElementById('drop-zone'); if(p6dz) p6dz.classList.add('next-step'); } } } catch(err){ const st = document.getElementById('bc-status'); if(st) st.innerHTML = '❌ Cannot read Excel — '+err.message+''; console.error(err); } }; rd.readAsArrayBuffer(file); } // Standalone template — independent of the P6/Cost engine above. Field staff // can open this, fill it in, and save/print each day without needing a P6 file. var drRowCounters = {manpower:0, equipment:0, photo:0}; function drRowTemplate(type, i){ if(type==='manpower'){ return ''+ ''+ ''+ ''+ ''+ ''; } if(type==='equipment'){ return ''+ ''+ ''+ ''+ ''+ ''; } if(type==='photo'){ return ''+ ''+ ''+ ''; } return ''; } function drAddRow(type){ var i = drRowCounters[type]++; var body = document.getElementById('dr-'+type+'-body'); if(body) body.insertAdjacentHTML('beforeend', drRowTemplate(type, i)); } function drRemoveRow(type, i){ var row = document.getElementById('dr-'+type+'-row-'+i); if(row) row.remove(); } function openDailyReport(){ var dateEl = document.getElementById('dr-date'); if(dateEl && !dateEl.value){ dateEl.value = new Date().toISOString().slice(0,10); } // Start with one blank row in each table if empty ['manpower','equipment','photo'].forEach(function(type){ var body = document.getElementById('dr-'+type+'-body'); if(body && body.children.length===0) drAddRow(type); }); document.getElementById('dr-overlay').classList.add('open'); document.body.classList.add('dr-printing'); } function closeDailyReport(){ document.getElementById('dr-overlay').classList.remove('open'); document.body.classList.remove('dr-printing'); } function selectDailyReportFile(){ openDailyReport(); document.getElementById('dr-file-input').click(); } function drCollectFieldIds(){ return ['dr-project','dr-jobno','dr-date','dr-weather-am','dr-weather-pm','dr-site-cond', 'dr-super','dr-preparedby','dr-totalworkers','dr-workdone','dr-materials','dr-visitors', 'dr-toolbox','dr-incidents','dr-safetyobs','dr-delays','dr-notes']; } function drCollectTableData(type){ var body = document.getElementById('dr-'+type+'-body'); if(!body) return []; return Array.from(body.querySelectorAll('tr')).map(function(tr){ return Array.from(tr.querySelectorAll('input')).map(function(inp){ return inp.value; }); }); } function drFillTableData(type, rows){ var body = document.getElementById('dr-'+type+'-body'); if(!body) return; body.innerHTML=''; drRowCounters[type]=0; (rows||[]).forEach(function(rowVals){ drAddRow(type); var tr = body.lastElementChild; var inputs = tr.querySelectorAll('input'); rowVals.forEach(function(v,idx){ if(inputs[idx]) inputs[idx].value = v||''; }); }); if(!rows || rows.length===0) drAddRow(type); } function saveDailyReport(){ var data = {version:'WTG-DailyReport-v1', savedAt:new Date().toISOString(), fields:{}}; drCollectFieldIds().forEach(function(id){ var el = document.getElementById(id); if(el) data.fields[id] = el.value; }); data.manpower = drCollectTableData('manpower'); data.equipment = drCollectTableData('equipment'); data.photos = drCollectTableData('photo'); var proj = (data.fields['dr-project']||'Project').replace(/[^a-z0-9]+/gi,'_'); var date = data.fields['dr-date']||new Date().toISOString().slice(0,10); var blob = new Blob([JSON.stringify(data,null,2)], {type:'application/json'}); var url = URL.createObjectURL(blob); var a = document.createElement('a'); a.href = url; a.download = 'WTG_DailyReport_'+proj+'_'+date+'.json'; a.click(); URL.revokeObjectURL(url); showToast('✅ Daily report saved — '+date); } function loadDailyReport(){ document.getElementById('dr-file-input').click(); } function onDailyReportFileLoad(e){ var file = e.target.files[0]; if(!file) return; var reader = new FileReader(); reader.onload = function(ev){ try{ var obj = JSON.parse(ev.target.result); if(!obj.fields) throw new Error('Invalid file'); Object.keys(obj.fields).forEach(function(id){ var el = document.getElementById(id); if(el) el.value = obj.fields[id]; }); drFillTableData('manpower', obj.manpower); drFillTableData('equipment', obj.equipment); drFillTableData('photo', obj.photos); showToast('✅ Daily report loaded — '+(obj.fields['dr-date']||'')); } catch(err){ showToast('❌ Could not read file — please use a WTG Daily Report file'); } }; reader.readAsText(file); e.target.value = ''; } function clearDailyReport(){ if(!confirm('Clear all fields on this form? This cannot be undone.')) return; drCollectFieldIds().forEach(function(id){ var el = document.getElementById(id); if(el) el.value=''; }); ['manpower','equipment','photo'].forEach(function(type){ var body = document.getElementById('dr-'+type+'-body'); if(body) body.innerHTML=''; drRowCounters[type]=0; drAddRow(type); }); document.getElementById('dr-date').value = new Date().toISOString().slice(0,10); }