/* =====================================================================
   NOWILLS SQC ERP — Module pack 5: 21 new quarry-ERP modules
   Specs §3–§53: qsite, geology, extract, products, pricing, dispatch,
   comms, hire, boq, plan, costing, tenders, mrp, payroll, rehab, qc,
   gate, fixedassets, loans, sync (+ mobile apps preview)
   ===================================================================== */
(function(){
'use strict';
const D=DATA,D2=DATA2,D3=DATA3,K=D.K,ts=D.ts,MON=D.MONTHS,C=Charts.palette;
const H=window.MH, S=H.S, ln=H.ln, ar=H.ar, br=H.br, hb=H.hb, dn=H.dn, pi=H.pi, gg=H.gg, wf=H.wf, hm=H.hm, scp=H.scp, fnl=H.fnl, mp=H.mp, item=H.item, fct=H.fct;
const DAYS30=H.DAYS30,HRS=H.HRS, MOUNT=window.MOUNT;
const esc=s=>String(s==null?'':s).replace(/&/g,'&amp;').replace(/</g,'&lt;');
function f(){return (window.NWX&&NWX.scope)?NWX.scope().f:1;}

/* ============================ QUARRY SITE MANAGEMENT ================= */
MOUNT.qsheet=function(el){ let cur=window.__qs||0;
 function render(){ const q=D3.QUARRY_MASTERS[cur];
  el.innerHTML='<div class="hlist" style="display:flex;gap:7px;flex-wrap:wrap">'+D3.QUARRY_MASTERS.map((x,i)=>'<span class="chip '+(i===cur?'on':'')+'" data-i="'+i+'">'+x.code+' — '+x.name+'</span>').join('')+
   '<span style="margin-left:auto"><button class="btn sm" id="qNew">'+NWX.icon('plus',13)+' Register new quarry</button></span></div>'+
  '<div class="statgrid" style="grid-template-columns:repeat(auto-fit,minmax(215px,1fr))">'+
   [['Quarry code',q.code],['Quarry name',q.name],['Location',q.loc],['GPS coordinates',q.gps],['Land size',q.land+' ha'],['Ownership',q.own],['Lease',q.lease],['Quarry licence',q.ql],['Mining permit',q.mp],['Environmental permit',q.ep],['Operating permit',q.op],['Responsible manager',q.mgr],['Geologist',q.geo],['Surveyor',q.svy],['Production capacity',q.pcap],['Crushing capacity',q.ccap],['Stockyard capacity',q.scap],['Working hours',q.hours],['Status','<span class="badge green">'+q.st+'</span>']].map(x=>'<div class="statmini" style="text-align:left;padding:10px 12px"><span>'+x[0]+'</span><b style="font-size:12px;font-weight:600">'+esc(x[1])+'</b></div>').join('')+'</div>'+
  '<div class="f-group" style="padding:0 12px 12px"><label>Operational areas — live status</label><div class="checks">'+q.areas.map(a=>'<span class="chip"><span class="dot" style="background:var(--green)"></span>'+a[0]+'</span>').join('')+'</div></div>';
  el.querySelectorAll('[data-i]').forEach(c=>c.onclick=()=>{cur=+c.dataset.i;window.__qs=cur;render();});
  el.querySelector('#qNew').onclick=()=>NWX.modal('Register quarry','<div class="form-grid">'+[['Quarry code'],['Quarry name'],['Location'],['GPS coordinates'],['Land size (ha)'],['Ownership status'],['Responsible manager']].map(x=>'<div class="full"><label>'+x[0]+'</label><input type="text"></div>').join('')+'</div><p style="font-size:11px;color:var(--muted)">Creates master in draft — licences & permits attach via DMS; publishing passes change control.</p>',null,'Create draft'); };
 render();};
MODULES.qsite=function(M){
 const cards=[K('Quarries Registered',3,{u:'#',d:0,neutral:1}),K('Active Sites',3,{u:'#',d:0}),
  K('Land Under Title',830,{u:'num',info:'hectares'}),K('Daily Production Capacity',10800,{u:'t'}),
  K('Crushing Capacity',1580,{u:'num',info:'tph combined'}),K('Stockyard Capacity',303e3,{u:'t'}),
  K('Permits Due < 90 days',3,{u:'#',good:'down',d:0}),K('Permits Overdue',0,{u:'#',good:'down',d:0,st:'green'}),
  K('Operational Areas',34,{u:'#',d:0,neutral:1}),K('Utilised Capacity',89.2,{u:'%',tgt:85})];
 const secs=[
  {k:'html',id:'qsheet',ti:'Quarry Master Records — full statutory & capacity profile',cls:'wide'},
  {k:'chart',id:'qcap',ti:'Capacity vs Output (t/day)',spec:br(['Shai Hills','Buoho','Nsawam South'],[S('Capacity',fct([5800,3200,1800],1),C[8]),S('Avg output',fct([5120,2880,1440],1),C[0]),S('Today',fct([Math.round(D.LIVE.prodToday*.52),Math.round(D.LIVE.prodToday*.31),Math.round(D.LIVE.prodToday*.17)],1),C[2])],{fmt:'num'})},
  {k:'chart',id:'qland',ti:'Land Bank (ha)',spec:hb(['Shai Hills','Buoho','Nsawam South'],[S('Total',[412,268,150],C[1]),S('Mined to date',[96,41,18],C[4])],{fmt:'num'})},
  {k:'chart',id:'qpermit',ti:'Permit Days-to-Expiry',spec:hb(['EPA — Nsawam NS-0661','EPA — Buoho BU-2219','MinCom MP-1990','MinCom OP-6612','QL-1187 Shai','MP-2200 Shai','LAND lease L-2009','QL-0912 Buoho'],[S('Days',[2,21,32,95,206,143,2956,32],C[4])],{fmt:'num'})},
  {k:'table',id:'qreg',ti:'Permit & Licence Register (all quarries)',cls:'wide',cfg:{id:'qreg',page:9,search:1,cols:[{k:'a',l:'Instrument'},{k:'b',l:'Number'},{k:'c',l:'Quarry'},{k:'d',l:'Authority'},{k:'e',l:'Expiry',y:'date'},{k:'f',l:'Days',y:'num'},{k:'g',l:'Status',y:'badge'}],rows:[
   {a:'Quarry licence',b:'QL-1187',c:'Shai Hills',d:'Minerals Commission',e:'2027-03-01',f:550,g:'green',gL:'Valid'},
   {a:'Mining permit',b:'MP-2200',c:'Shai Hills',d:'Minerals Commission',e:'2026-12-01',f:113,g:'green',gL:'Valid'},
   {a:'EPA permit',b:'EPA-GRA-2218',c:'Shai Hills',d:'EPA Ghana',e:'2027-01-15',f:158,g:'green',gL:'Valid'},
   {a:'EPA permit',b:'BU-2219',c:'Buoho',d:'EPA Ghana',e:'2026-08-31',f:21,g:'amber',gL:'Renewal filed'},
   {a:'Land lease',b:'L-2009-SH',c:'Shai Hills',d:'Lands Commission',e:'2034-06-30',f:2956,g:'green',gL:'Valid'},
   {a:'EPA EIA certificate',b:'NS-0661',c:'Nsawam South',d:'EPA Ghana',e:'2026-08-12',f:2,g:'red',gL:'Renew urgently'},
   {a:'Blasting permit',b:'MC-BL-2210',c:'Shai Hills',d:'Minerals Commission',e:'2026-10-31',f:82,g:'green',gL:'Valid'},
   {a:'Operating permit',b:'OP-6612',c:'Buoho',d:'Minerals Commission',e:'2026-11-01',f:83,g:'green',gL:'Valid'},
   {a:'Magazine licence',b:'MAG-5520',c:'All sites',d:'Minerals Commission',e:'2026-12-14',f:126,g:'green',gL:'Valid'}]}},
  {k:'chart',id:'qarea',ti:'Area Downtime Incidents (MTD, by area)',spec:br(['Pit','Crusher hall','Screening','Stockpile','Weighbridge','Workshop','Fuel stn','Magazine'],[S('Incidents',[2,5,3,1,1,2,0,0],C[3])],{fmt:'num'})}
 ];
 return {title:'qsite',sub:'Master records · permits · areas · capacities',cards:cards,sections:secs};
};

/* ============================ GEOLOGY & RESERVES ================= */
MODULES.geology=function(M){
 const tot=(k)=>D3.RESERVES.reduce((a,r)=>a+r[k],0);
 const cards=[K('Proven Reserves',7.8e6,{u:'t',noscope:1}),K('Probable Reserves',4.0e6,{u:'t',noscope:1}),
  K('Extracted to Date',1.66e6,{u:'t',noscope:1}),K('Remaining Reserves',10.1e6,{u:'t',noscope:1}),
  K('Overburden to Strip',0.95e6,{u:'t',noscope:1}),K('Waste Rock Ratio',9.4,{u:'%',good:'down',d:1,noscope:1}),
  K('Avg Quarry Life',6.1,{u:'num',d:1,info:'years at current extraction',noscope:1}),
  K('Drill Holes Logged',412,{u:'#',d:0,noscope:1}),K('Blocks Mapped',6,{u:'#',d:0,noscope:1}),
  K('Survey Variance',1.8,{u:'%',good:'down',d:1}),K('Grade Recovery (ACV pass)',96,{u:'%',tgt:95})];
 const mn=['Sep','Oct','Nov','Dec','Jan','Feb','Mar'];
 const secs=[
  {k:'chart',id:'gdep',ti:'Reserve Depletion & Life-of-Mine Forecast (Mt)',cls:'wide',spec:ln(['2024','2025*','2026*','2027*','2028*','2029*','2030*'],[S('Remaining (proven+probable)',[13.4,12.1,10.1,8.3,6.6,4.9,3.3],C[0],{area:true}),S('AI depletion forecast',Array(2).fill(null).concat([10.1,8.1,6.2,4.4,2.7]),C[6],{dash:true})],{fmt:'t',ann:[{i:2,text:'today'}],height:290,drillDim:'Reserve ledger'})},
  {k:'chart',id:'gres',ti:'Reserves by Block (Mt)',spec:br(D3.RESERVES.map(r=>r.block),[S('Proven',D3.RESERVES.map(r=>Math.round(r.proven/1e5)/10),C[0]),S('Probable',D3.RESERVES.map(r=>Math.round(r.probable/1e5)/10),C[8])],{fmt:'num',stacked:true})},
  {k:'chart',id:'glife',ti:'Remaining Life by Block (years)',spec:hb(D3.RESERVES.map(r=>r.block+' · '+r.bench),[S('Years',D3.RESERVES.map(r=>r.life),C[1])],{fmt:'num'})},
  {k:'chart',id:'gstrip',ti:'Overburden / Waste vs Reserve (Mt)',spec:br(D3.RESERVES.map(r=>r.block),[S('Overburden',D3.RESERVES.map(r=>Math.round(r.overburden/1e4)/100),C[4]),S('Waste rock',D3.RESERVES.map(r=>Math.round(r.waste/1e4)/100),C[6]),S('Saleable reserve',D3.RESERVES.map(r=>Math.round((r.proven+r.probable)/1e5)/10),C[2])],{fmt:'num'})},
  {k:'chart',id:'gqual',ti:'Rock Quality by Block',spec:hb(D3.RESERVES.map(r=>r.block),[S('ACV',[21,24,22,25,23,26],C[2]),S('LAA',[18,21,19,23,20,24],C[1]),S('SG ×10',[27.1,26.6,26.8,26.4,26.9,26.3],C[0])],{fmt:'num'})},
  {k:'chart',id:'gsurv',ti:'Survey Variance — physical vs model (%)',spec:ln(DAYS30.slice(-14),[S('Variance',ts(14,1.6,.5,0,'gsv'),C[3])],{fmt:'pct',min:0,max:4})},
  {k:'map',id:'geomap',ti:'GIS — blocks, benches & drone survey footprints',...{}&&{},height:380},
  {k:'table',id:'gdrill',ti:'Exploration Drill-Hole Log',cls:'wide',cfg:{id:'gdrill',page:8,cols:[{k:'a',l:'Hole'},{k:'b',l:'Block'},{k:'c',l:'Depth (m)',y:'num'},{k:'d',l:'Recovery %',y:'pct'},{k:'e',l:'Rock type'},{k:'f',l:'RQD %',y:'num'},{k:'g',l:'Logged by'},{k:'h',l:'Date',y:'date'}],rows:[['DH-221','A-North',32.5,96,'Granite (fresh)',82,'Dr. A. Owusu','2026-07-14'],['DH-222','A-North',28.0,93,'Granite',78,'Dr. A. Owusu','2026-07-14'],['DH-223','B-South',35.0,97,'Granodiorite',85,'M. Frimpong','2026-07-29'],['DH-224','B-West',22.4,88,'Granodiorite (weathered)',61,'M. Frimpong','2026-07-29'],['DH-225','N-1',30.0,95,'Gneiss',80,'R. Asante','2026-08-02'],['DH-226','N-2',26.0,91,'Gneiss',74,'R. Asante','2026-08-02']].map(x=>({a:x[0],b:x[1],c:x[2],d:x[3],e:x[4],f:x[5],g:x[6],h:x[7]}))}},
  {k:'html',id:'georep',ti:'Geology Reports — one click',cls:'wide'}
 ];
 return {title:'geology',sub:'Reserves · blocks · surveys · life-of-mine',cards:cards,sections:secs};
};
MOUNT.geomap=function(el){ const mk=D3.QUARRY_MASTERS.map(q=>({kind:'quarry',lon:q.lon,lat:q.lat,label:q.code+' '+q.name,tag:q.code,detail:'Blocks live — reserves 10.1 Mt remaining group-wide'}))
  .concat([{kind:'site',lon:-0.05,lat:5.90,label:'Bench L4N drone swath',tag:'DRN',detail:'Photogrammetry 06 Aug · 1.2 cm GSD · 40.6k m³'}]);
 Charts.render(el,{type:'map',markers:mk,layers:['quarry','site'],height:380}); };
MOUNT.georep=function(el){ const reps=[['Reserve statement (JORC-style)','Proven/probable by block, cut-off & licences'],['Remaining quarry life','By block & production scenario'],['Extraction by bench','Tonnage, yield, ore loss'],['Rock-quality compendium','ACV/AIV/LAA/SG/flakiness history'],['Survey variance report','Drone vs belt-scale reconciliation'],['Monthly depletion report','Board pack insert']];
 el.innerHTML='<div class="tiles" style="grid-template-columns:repeat(auto-fill,minmax(220px,1fr))">'+reps.map(r=>'<div class="tile" data-r="'+r[0]+'" style="align-items:flex-start;text-align:left"><b>'+r[0]+'</b><span style="color:var(--muted);font-weight:400;font-size:11px">'+r[1]+'</span></div>').join('')+'</div>';
 el.querySelectorAll('.tile').forEach(t=>t.onclick=()=>NWX.runReport(t.dataset.r));};

/* ============================ EXTRACTION & LOADING ================= */
MODULES.extract=function(M){
 const rows=D3.extOrders(); const f1=f();
 const ton=rows.reduce((a,r)=>a+r.act,0);
 const cards=[K('Extraction Orders (wk)',16,{u:'#',d:0}),K('Loads Hauled (wk)',588,{u:'#',d:0}),
  K('Tonnes to Crusher Feed',48200,{u:'t',noscope:0}),K('Waste Hauled',3200,{u:'t',good:'down'}),
  K('Equipment Hours (wk)',608,{u:'h'}),K('Production Variance',-4.1,{u:'%',good:'up',d:1,st:'amber'}),
  K('Fuel — Extraction',9860,{u:'L',good:'down'}),K('Excavator Utilization',81,{u:'%',tgt:78}),
  K('Loader Utilization',76,{u:'%',tgt:78}),K('Downtime (wk)',12.4,{u:'h',good:'down',d:1})];
 const secs=[
  {k:'chart',id:'exh',ti:'Hourly Extraction (t)',cls:'wide',spec:ln(HRS.slice(6,20),[S('Shai Hills',fct(ts(14,338,.14,0,'ex1'),f1),C[0]),S('Buoho',fct(ts(14,186,.16,0,'ex2'),f1),C[1]),S('Nsawam',fct(ts(14,92,.18,0,'ex3'),f1),C[3])],{fmt:'t',live:true,tickLabel:n=>'+'+n+'h',height:280})},
  {k:'chart',id:'exb',ti:'Loads by Bench (wk)',spec:br(['L4N','L3E','B2S','B1W','N1','N2'],[S('Loads',fct([168,142,118,74,52,34],f1),C[1])],{fmt:'num'})},
  {k:'chart',id:'exr',ti:'Tonnes by Rock Type (wk)',spec:dn([item('Granite',Math.round(29400*f1),C[0]),item('Granodiorite',Math.round(14200*f1),C[1]),item('Gneiss',Math.round(4600*f1),C[3]),item('Overburden',Math.round(3200*f1),C[8])],{fmt:'t',center:'Wk'})},
  {k:'chart',id:'exv',ti:'Estimate vs Actual (t/order)',spec:br(DAYS30.slice(-10),[S('Estimated',fct(ts(10,420,.1,0,'exv1'),f1),C[8]),S('Actual',fct(ts(10,404,.11,0,'exv2'),f1),C[0])],{fmt:'t'})},
  {k:'chart',id:'exd',ti:'Downtime Pareto (h/wk)',spec:hb(['Hydraulic','Waiting trucks','Blast clearance','Fuel stop','Electrical','Tyres','Greasing'],[S('h',[4.2,3.1,2.2,1.4,0.8,0.4,0.3],C[6])],{fmt:'h'})},
  {k:'chart',id:'exf',ti:'Fuel per Load (L)',spec:ln(DAYS30.slice(-14),[S('L/load',ts(14,16.8,.07,-.004,'exf'),C[2],{area:true})],{fmt:'L',target:{value:18,label:'norm'}})},
  {k:'table',id:'ext',ti:'Extraction Orders — live register',cls:'wide',cfg:{id:'ext',page:9,search:1,cols:[{k:'no',l:'Order'},{k:'bench',l:'Bench'},{k:'rock',l:'Rock'},{k:'ex',l:'Excavator'},{k:'ld',l:'Loader'},{k:'op',l:'Operator'},{k:'st',l:'Start'},{k:'en',l:'End'},{k:'hrs',l:'Eq. hrs',y:'num'},{k:'loads',l:'Loads',y:'num'},{k:'est',l:'Est. t',y:'num'},{k:'act',l:'Actual t',y:'num'},{k:'vr',l:'Var %',y:'pct'},{k:'dest',l:'Destination'},{k:'fuel',l:'Fuel L',y:'num'},{k:'down',l:'Down h',y:'num'},{k:'sup',l:'Supervisor'}],rows:rows,totals:['loads','est','act','fuel']}}
 ];
 return {title:'extract',sub:'Pit floor • load & haul • variance',cards:cards,sections:secs};
};

/* ============================ PRODUCTS ================= */
MOUNT.pcat=function(el){
 el.innerHTML='<div class="piles">'+D3.PCAT.map((p,i)=>'<div class="pile" data-i="'+i+'"><div style="display:flex;gap:8px;align-items:center;margin-bottom:5px"><span class="badge blue">'+p.code+'</span><span class="badge '+(p.grade==='A'?'green':p.grade==='B'?'amber':'grey')+'">Grade '+p.grade+'</span>'+(p.active?'<span class="badge green" style="margin-left:auto">Active</span>':'')+'</div>'+
  '<div style="font-weight:700">'+p.name+'</div><div style="font-size:11px;color:var(--muted)">'+p.mat+' · '+p.size+' · '+p.dens+'</div>'+
  '<div style="display:flex;justify-content:space-between;margin-top:7px"><b style="font-size:17px">'+NWX.fmtM(p.price)+'<small style="font-weight:500;color:var(--muted)">/'+p.uom+'</small></b><span style="font-size:11px;color:var(--muted)">min '+p.min+' t</span></div>'+
  '<div style="font-size:10.5px;color:var(--faint);margin-top:5px">'+p.use+' · '+p.site+' · '+p.tax+'</div></div>').join('')+'</div>';
 el.querySelectorAll('.pile').forEach(c=>c.onclick=()=>{ const p=D3.PCAT[+c.dataset.i];
  NWX.modal(p.code+' — '+p.name,'<div class="statgrid">'+[['Material',p.mat],['Size',p.size],['Grade',p.grade],['UoM',p.uom],['Density',p.dens],['Standard price',NWX.fmtM(p.price)+'/'+p.uom],['Min order',p.min+' t'],['Tax code',p.tax],['Stockpile',p.site],['Intended use',p.use]].map(x=>'<div class="statmini" style="text-align:left"><span>'+x[0]+'</span><b style="font-size:12px">'+x[1]+'</b></div>').join('')+'</div>'+
  '<div class="ck-body" id="pHist" style="height:200px;margin-top:10px"></div><div style="display:flex;gap:8px;margin-top:10px"><button class="btn sm" id="pBook">Customer-specific prices</button><button class="btn sm ghost" id="pSpec">Quality spec</button></div>',
  b2=>{ Charts.render(b2.querySelector('#pHist'),{type:'line',title:'Price history',labels:['Mar','Apr','May','Jun','Jul','Aug'],series:[{name:'List price',data:D.ts(6,p.price*.94,.02,.01,'ph'+p.code),color:C[0]}],fmt:'money'});
   b2.querySelector('#pBook').onclick=()=>NWX.toast('Price book','4 customer-specific prices loaded — dealer −₵15, contract −₵12, delivered +₵28.','blue');
   b2.querySelector('#pSpec').onclick=()=>NWX.toast('Quality spec','ACV ≤25 · LAA ≤30 · FI ≤20% · SG 2.6–2.75 · WA ≤0.6% — lab current ✓','green'); }); });};
MODULES.products=function(M){
 const f1=f();
 const cards=[K('Active SKUs',14,{u:'#',d:0,neutral:1}),K('Avg List Price',196,{u:'GHS',d:0}),
  K('Gross Margin (avg)',38,{u:'%',tgt:36}),K('Below Margin Floor',1,{u:'#',good:'down',d:0}),
  K('Customer-Specific Prices',58,{u:'#',d:0,neutral:1}),K('Contract Prices',22,{u:'#',d:0}),
  K('Uses Covered',9,{u:'#',d:0,neutral:1}),K('New SKUs (Q)',2,{u:'#',d:0}),K('Inactive SKUs',1,{u:'#',good:'down',d:0})];
 const secs=[
  {k:'html',id:'pcat',ti:'Product Catalogue — 14 governed SKUs',cls:'wide'},
  {k:'chart',id:'ppr',ti:'List Price by Product (₵/t)',spec:br(D3.PCAT.map(p=>p.size),[S('₵/t',D3.PCAT.map(p=>p.price),C[0])],{fmt:'money'})},
  {k:'chart',id:'pmg',ti:'Price vs Estimated Cost (bubble = volume)',spec:scp(D3.PCAT.map((p,i)=>({x:Math.round(p.price*0.62+((i*13)%9)),y:p.price,label:p.name,r:6+((i*7)%12),c:p.price<100?'#f59e0b':'#f8c200'})),{xl:'Est. full cost ₵/t',yl:'List ₵/t',refX:140,refY:140})},
  {k:'chart',id:'pmix',ti:'Sales Mix by Product (t MTD)',spec:dn(D3.PCAT.slice(0,7).map((p,i)=>item(p.size,Math.round([21.4,25.6,18.3,9.2,12.8,4.4,8.6][i]*1e3),C[i%C.length])),{fmt:'t',center:'MTD'})},
  {k:'chart',id:'puse',ti:'Revenue by Intended Use (₵M)',spec:hb(['Road base','Concrete','Asphalt','Block/paving','Fill','Coastal','Sub-base','Rail/landscape'],[S('₵M',[7.9,5.4,3.1,1.8,0.9,0.7,0.5,0.2],C[1])],{fmt:'num'})},
  {k:'table',id:'pcatt',ti:'Catalogue Register — every governed field',cls:'wide',cfg:{id:'pcatt',page:10,search:1,cols:[{k:'code',l:'Code'},{k:'name',l:'Product'},{k:'mat',l:'Material'},{k:'size',l:'Size'},{k:'grade',l:'Grade'},{k:'uom',l:'UoM'},{k:'dens',l:'Density'},{k:'price',l:'Std price',y:'money'},{k:'min',l:'Min order',y:'num'},{k:'use',l:'Intended use',wide:1},{k:'site',l:'Stockpile'},{k:'tax',l:'Tax code'},{k:'active',l:'Status',y:'badge',badge:v=>v?'green':'grey'}],rows:D3.PCAT.map(p=>Object.assign({},p,{activeL:p.active?'Active':'Inactive'}))}}
 ];
 return {title:'products',sub:'Catalogue · pricing governance · quality specs',cards:cards,sections:secs};
};

/* ============================ PRICING & DISCOUNTS ================= */
MODULES.pricing=function(M){
 const f1=f();
 const cards=[K('Price Lists Active',5,{u:'#',d:0,neutral:1}),K('Price Rules',58,{u:'#',d:0}),
  K('Overrides Pending',4,{u:'#',d:0,good:'down'}),K('Avg Discount (YTD)',6.8,{u:'%',d:1,good:'down'}),
  K('Margin Breaches (MTD)',2,{u:'#',good:'down',d:0}),K('Prices Expiring (30d)',5,{u:'#',d:0}),
  K('Unauthorized Changes',0,{u:'#',d:0,st:'green'}),K('Early-Pmt Discount Cost',38.2e3,{u:'GHS',good:'down'}),K('Seasonal Rules Live',2,{u:'#',d:0,neutral:1})];
 const secs=[
  {k:'chart',id:'px1',ti:'Price vs Floor — margin protection',cls:'wide',spec:scp(D3.PRX.map((p,i)=>({x:p.minSell,y:p.std,label:p.name,r:8,c:p.std<p.minSell*1.02?'#ef4444':'#22c55e'})),{xl:'Min selling ₵',yl:'Actual list ₵',refY:150,height:280})},
  {k:'chart',id:'px2',ti:'Discount Rate Trend (%)',spec:ln(MON,[S('Avg discount',ts(12,6.2,.2,.01,'pxd'),C[4],{area:true}),S('Volume disc.',ts(12,4.8,.2,0,'pxv'),C[1]),S('Early-pmt',ts(12,1.6,.3,0,'pxe'),C[2])],{fmt:'pct'})},
  {k:'chart',id:'px3',ti:'Price Realisation vs List (%)',spec:hb(D3.PRX.map(p=>p.name.slice(0,14)),[S('%',fct([97,99,101,96,94,98,92,100],1),C[0])],{fmt:'pct'})},
  {k:'chart',id:'pxhist',ti:'Aggregate 20mm — price history & effective dating',spec:ln(['Mar','Apr','May','Jun','Jul','Aug','Sep*'],[S('Standard',[178,182,186,189,192,195,198],C[0]),S('Contract',[168,170,174,178,180,183,186],C[3]),S('Dealer',[162,166,168,171,175,180,183],C[5])],{fmt:'money'})},
  {k:'table',id:'pxm',ti:'Price Matrix (effective-dated — edit stages approval)',cls:'wide',cfg:{id:'pxm',page:10,cols:[{k:'code',l:'SKU'},{k:'name',l:'Product'},{k:'std',l:'Standard',y:'money'},{k:'qry',l:'Ex-quarry',y:'money'},{k:'del',l:'Delivered',y:'money'},{k:'dealer',l:'Dealer',y:'money'},{k:'contract',l:'Contract',y:'money'},{k:'minSell',l:'Min sell',y:'money'},{k:'eff',l:'Effective',y:'date'},{k:'exp',l:'Expiry',y:'date'},{k:'ch',l:'Pending change',y:'badge',badge:v=>v==='—'?'grey':'amber'}],rows:D3.PRX.map(p=>Object.assign({},p,{ch:p.ch,chL:p.ch==='—'?'—':'Pending approval'}))}},
  {k:'table',id:'pxap',ti:'Discount / Override Approval Queue',cls:'wide',cfg:{id:'pxap',page:6,cols:[{k:'a',l:'Ref'},{k:'b',l:'Customer'},{k:'c',l:'Product'},{k:'d',l:'Ask price',y:'money'},{k:'e',l:'Vs list',y:'pct'},{k:'f',l:'Margin',y:'pct'},{k:'g',l:'Requester'},{k:'h',l:'Status',y:'select',opts:['Pending','Approve','Reject','Escalate to CFO']}],rows:[
   {a:'DX-118',b:'Goldline Civil Works',c:'Aggregate 20mm',d:182,e:-6.7,f:31,g:'A. Serwaa',h:'Pending'},
   {a:'DX-119',b:'KMA / AshGold',c:'Base course GS',d:150,e:-9.1,f:22,g:'K. Ofori',h:'Escalate to CFO'},
   {a:'DX-120',b:'Tema Port Expansion',c:'Riprap',d:395,e:-6.0,f:36,g:'N. Ashong',h:'Approve'},
   {a:'DX-121',b:'Volta Infra Group',c:'Crusher run',d:128,e:-5.2,f:29,g:'D. Bonsu',h:'Pending'}]}}
 ];
 return {title:'pricing',sub:'Price book · floors · discounts · approvals',cards:cards,sections:secs};
};

/* ============================ DISPATCH & DELIVERY ================= */
MOUNT.dspboard=function(el){ const rows=D3.dispatches();
 el.innerHTML='<div style="display:flex;gap:9px;overflow-x:auto;padding:10px 12px">'+D3.DSTAT.map(s2=>{ const r=rows.filter(x=>x.st===s2);
  return '<div class="kb-col" style="min-width:172px;flex:0 0 172px"><h6>'+s2+' <span class="cnt">'+r.length+'</span></h6>'+r.map(x=>'<div class="kb-card" data-d="'+x.no+'"><b>'+x.no+'</b><div style="font-size:10.5px;color:var(--muted)">'+x.veh+' · '+x.prod.split('(')[0]+'</div><div style="font-size:10.5px;color:var(--muted)">'+x.cust+' → '+x.dest+'</div></div>').join('')+(!r.length?'<div class="empty-state" style="padding:12px;font-size:11px">—</div>':'')+'</div>';}).join('')+'</div>';
 el.querySelectorAll('.kb-card').forEach(c=>c.onclick=()=>{ const d2=rows.find(x=>x.no===c.dataset.d);
  NWX.modal('Dispatch '+d2.no,'<div class="statgrid">'+[['Vehicle',d2.veh],['Driver',d2.drv],['Customer',d2.cust],['Product',d2.prod],['Destination',d2.dest],['ETA',d2.eta],['Arrived',d2.arr],['Status',d2.st],['Variance',d2.var],['Proof of delivery',d2.pod]].map(x=>'<div class="statmini" style="text-align:left"><span>'+x[0]+'</span><b style="font-size:12px">'+x[1]+'</b></div>').join('')+'</div>'+
  '<div style="display:flex;gap:8px;margin-top:12px"><button class="btn sm" id="dTrack">Track on map</button><button class="btn sm" id="dMsg">Notify customer</button>'+(d2.st==='Pending'?'<button class="btn sm pri" id="dAp">Approve dispatch</button>':'')+'</div>',
  b2=>{ b2.querySelector('#dTrack').onclick=()=>{NWX.go('fleet');}; b2.querySelector('#dMsg').onclick=()=>NWX.toast('Customer notified','ETA SMS + WhatsApp sent with live tracking link.','green');
   const db=b2.querySelector('#dAp'); if(db)db.onclick=()=>{d2.st='Approved';NWX.toast('Approved','Dispatch authorized — gate + weighbridge pre-registered.','green');NWX.audit('Dispatch approved',d2.no);b2.closest('.modal-ov').remove();}; }); });};
MODULES.dispatch=function(M){
 const f1=f();const rows=D3.dispatches();
 const cards=[K('Dispatches Today',34,{u:'#',d:0}),K('On the Road',11,{u:'#',d:0,neutral:1}),
  K('Delivered (wk)',138,{u:'#',d:0}),K('On-time %',93.4,{u:'%',tgt:95,d:1}),
  K('ETA At Risk',4,{u:'#',good:'down',d:0}),K('Partial Deliveries',2,{u:'#',good:'down',d:0}),
  K('Returned Loads',1,{u:'#',good:'down',d:0}),K('Avg Turnaround',4.6,{u:'h',good:'down',d:1,tgt:4}),
  K('Delivery Charges Collected',86e3,{u:'GHS'}),K('e-POD Coverage',96,{u:'%',tgt:98})];
 const secs=[
  {k:'html',id:'dspboard',ti:'Dispatch Pipeline — every live order by status',cls:'wide',live:1},
  {k:'chart',id:'dpf',ti:'Pipeline Conversion (week)',spec:fnl([item('Scheduled',214,C[8]),item('Approved',198,C[1]),item('Loaded & weighed',186,C[4]),item('Dispatched',181,C[0]),item('Delivered',171,C[2])],{fmt:'num'})},
  {k:'chart',id:'dpd',ti:'Deliveries per Day',spec:br(DAYS30.slice(-14),[S('Deliveries',fct(ts(14,32,.14,0,'dpd'),f1),C[0])],{fmt:'num'})},
  {k:'chart',id:'dpo',ti:'On-time Delivery %',spec:ln(DAYS30.slice(-14),[S('OT %',ts(14,93,.02,.002,'dpo'),C[2])],{fmt:'pct',min:85,max:100})},
  {k:'chart',id:'dpt',ti:'Turnaround by Route (h)',spec:hb(['Shai→Tema Port','Shai→P-2201','Buoho→Kumasi N','Buoho→P-2221','Nsawam→Pokuase'],[S('Hours',[3.9,4.4,5.8,3.1,5.2],C[3])],{fmt:'h'})},
  {k:'table',id:'dpr',ti:'Live Dispatch Register',cls:'wide',cfg:{id:'dpr',page:10,search:1,cols:[{k:'no',l:'Dispatch'},{k:'veh',l:'Vehicle'},{k:'drv',l:'Driver'},{k:'cust',l:'Customer'},{k:'prod',l:'Product'},{k:'dest',l:'Destination'},{k:'eta',l:'ETA',y:'date'},{k:'arr',l:'Arrived'},{k:'var',l:'Var'},{k:'pod',l:'e-POD'},{k:'st',l:'Status',y:'badge',badge:v=>({Pending:'grey',Approved:'blue',Queued:'blue',Loading:'amber',Weighed:'amber',Dispatched:'blue','In transit':'blue',Delivered:'green','Partially delivered':'amber',Returned:'red',Cancelled:'red'}[v]||'grey')}],rows:rows}}
 ];
 return {title:'dispatch',sub:'Pipeline · ETAs · e-POD · variances',cards:cards,sections:secs};
};

/* ============================ COMMUNICATION CENTRE ================= */
MOUNT.comlog=function(el){ let f2='All'; const rows=D3.commsLog();
 function render(){ el.innerHTML='<div class="hlist" style="display:flex;gap:6px;padding-bottom:6px">'+['All','SMS','Email','WhatsApp','Push'].map(c=>'<span class="chip '+(c===f2?'on':'')+'" data-c="'+c+'">'+c+'</span>').join('')+'<span style="margin-left:auto"><button class="btn sm ghost" id="mRetry">Re-send failed</button></span></div>'+
  '<div style="max-height:330px;overflow:auto">'+rows.filter(r=>f2==='All'||r.ch===f2).map(r=>'<div class="notif" style="cursor:default;padding:9px;margin:0 12px 6px"><div class="ni" style="width:30px;height:30px;background:var(--blue-d);color:var(--blue)">'+NWX.icon('mail',14)+'</div><div style="flex:1"><h5>'+r.ev+' <span class="badge '+(r.ch==='SMS'?'amber':r.ch==='Email'?'blue':r.ch==='WhatsApp'?'green':'grey')+'">'+r.ch+'</span></h5><p>→ '+r.to+(r.link!=='—'?' · ref '+r.link:'')+'</p></div><span class="badge '+(r.stat==='Delivered'?'green':r.stat==='Retry'?'amber':'red')+'">'+r.stat+'</span></div>').join('')+'</div>';
  el.querySelectorAll('[data-c]').forEach(c=>c.onclick=()=>{f2=c.dataset.c;render();});
  el.querySelector('#mRetry').onclick=()=>NWX.toast('Re-sent','2 failed + 1 retry messages re-queued — gateway healthy.','green'); }
 render();};
MODULES.comms=function(M){
 const cards=[K('Messages Today',312,{u:'#',d:0,neutral:1}),K('Delivered %',98.1,{u:'%',d:1,tgt:98}),
  K('Failed',3,{u:'#',good:'down',d:0}),K('Automation Rules',13,{u:'#',d:0,neutral:1}),
  K('Templates Governed',13,{u:'#',d:0}),K('Quiet Hours','21–06',{u:'text',neutral:1}),
  K('WhatsApp Coverage',71,{u:'%',d:0}),K('SMS Cost (MTD)',8.4e3,{u:'GHS',good:'down'})];
 const secs=[
  {k:'html',id:'comlog',ti:'Live Message Log — all channels',cls:'wide',live:1},
  {k:'chart',id:'cmc',ti:'Messages by Channel (today)',spec:dn([item('SMS',112,C[0]),item('Email',96,C[1]),item('WhatsApp',68,C[2]),item('Push',36,C[3])],{fmt:'num',center:'Msgs'})},
  {k:'chart',id:'cme',ti:'Messages by Event (week)',spec:hb(['Order confirmation','Delivery notification','Invoice','Payment receipt','Credit warning','Approval reminder','Maintenance alert','Licence expiry','Safety incident','Stock reorder'],[S('Count',[96,84,64,51,9,44,31,6,3,12],C[1])],{fmt:'num'})},
  {k:'chart',id:'cmd',ti:'Delivery Success Trend (%)',spec:ln(DAYS30.slice(-14),[S('%',ts(14,98,.01,0,'cmd'),C[2])],{fmt:'pct',min:94,max:100})},
  {k:'table',id:'cauto',ti:'Automation Rules — flip channels per event',cls:'wide',cfg:{id:'cauto',page:14,cols:[{k:'a',l:'Event'},{k:'b',l:'Module'},{k:'c',l:'Channels'},{k:'d',l:'Recipients'},{k:'e',l:'Template version'},{k:'f',l:'Status',y:'select',opts:['Active','Paused']} ],rows:D2.TPL.map(t=>({a:t[0],b:t[1],c:t[3].split(',').join(' · '),d:'Owner + line manager',e:'v3.2',f:t[0]==='Supplier payment due'?'Paused':'Active'}))}}
 ];
 return {title:'comms',sub:'Email · SMS · WhatsApp · push · automations',cards:cards,sections:secs};
};

/* ============================ EQUIPMENT HIRE ================= */
MODULES.hire=function(M){
 const f1=f();
 const cards=[K('Active Hire Contracts',5,{u:'#',d:0}),K('Units on Hire',5,{u:'#',d:0,neutral:1}),
  K('Hire Revenue (MTD)',2.6e6,{u:'GHS',tgt:2.9e6}),K('Hire Fleet Utilization',88,{u:'%',tgt:80}),
  K('Standby Hours (wk)',14,{u:'h',good:'down'}),K('Damage Charges Pending',1,{u:'#',d:0,good:'down'}),
  K('Invoices Outstanding',0.9e6,{u:'GHS',good:'down'}),K('Mobilization Fees MTD',12.6e3,{u:'GHS'}),
  K('Hire Margin',42,{u:'%',tgt:38}),K('Repeat Customers',4,{u:'#',d:0,neutral:1})];
 const secs=[
  {k:'chart',id:'hr1',ti:'Hire Revenue by Customer (₵ MTD)',spec:hb(D3.HIRES.map(h=>h.cust),[S('₵',fct([1.02e6,0.52e6,0.44e6,0.36e6,0.26e6],f1),C[0])],{fmt:'money'})},
  {k:'chart',id:'hr2',ti:'Hire Revenue Trend',spec:br(MON.slice(0,8),[S('₵',fct(ts(8,2.35e6,.09,.012,'hr2'),f1),C[2])],{fmt:'money'})},
  {k:'chart',id:'hr3',ti:'Rate Mix',spec:dn([item('Hourly',38,C[0]),item('Daily',41,C[1]),item('Weekly',12,C[3]),item('Monthly',9,C[4])],{fmt:'pct',center:'Mix'})},
  {k:'chart',id:'hr4',ti:'Utilization — Hire Fleet vs Own Fleet (%)',spec:br(['W28','W29','W30','W31','W32'],[S('Hire fleet',[88,91,86,90,88],C[0]),S('Own fleet',[74,76,73,78,81],C[8])],{fmt:'pct'})},
  {k:'table',id:'hr5',ti:'Hire Contracts Register',cls:'wide',cfg:{id:'hr5',page:8,search:1,cols:[{k:'no',l:'Contract'},{k:'cust',l:'Customer'},{k:'what',l:'Equipment package'},{k:'rate',l:'Rate & terms',wide:1},{k:'term',l:'Period'},{k:'hrs',l:'Hours'},{k:'bill',l:'Billing'},{k:'st',l:'Status',y:'badge',badge:v=>v==='Active'?'green':'grey'}],rows:D3.HIRES,totals:[]}},
  {k:'chart',id:'hr6',ti:'Invoice Aging — hire debtors (₵)',spec:br(['Current','1–30d','31–60d','>60d'],[S('₵',fct([520,260,84,36],1e3),C[4])],{fmt:'money'})}
 ];
 return {title:'hire',sub:'Dry/wet hire · rates · standby · billing',cards:cards,sections:secs};
};

/* ============================ BOQ ============================ */
MODULES.boq=function(M){
 const f1=f();
 const cards=[K('BOQ Items',1248,{u:'#',d:0,neutral:1}),K('Sections',38,{u:'#',d:0}),
  K('Completion (measured)',51.2,{u:'%',d:1}),K('Measured Value',113.6e6,{u:'GHS'}),
  K('Revised Items (VOs)',87,{u:'#',d:0}),K('Qty Variance',+2.4,{u:'%',good:'down',d:1,st:'amber'}),
  K('Rate Variance',-1.1,{u:'%',good:'up',d:1}),K('Items Locked (certified)',612,{u:'#',d:0}),K('Import Errors Open',0,{u:'#',d:0,st:'green'})];
 const boqRows=[['A.1','Site clearance','m²',148000,142,0.98,'Roads / Prelims'],['A.2','Topsoil strip & stockpile','m³',42000,38,1.02,'Earthworks'],['B.1','Cut to fill — common excavation','m³',186000,64,0.91,'Earthworks'],['B.2','Rock excavation (blasted)','m³',98000,118,1.06,'Earthworks'],['C.1','Sub-base GSB 200mm','m³',36000,205,0.97,'Pavement'],['C.2','Base course crushed rock 150mm','m³',24000,288,0.94,'Pavement'],['D.1','DBST surfacing 14/10','m²',92000,96,1.01,'Surfacing'],['D.2','Asphalt wearing 40mm','t',18100,1450,0.89,'Surfacing'],['E.1','uPVC drains Ø600','m',8400,615,0.99,'Drainage'],['E.2','Box culvert 2×2m','no',6,4.1e5,0.72,'Structures'],['F.1','Concrete C30 structural','m³',5200,2680,0.66,'Structures'],['F.2','Rebar H16','t',412,9850,0.71,'Structures']].map(x=>({it:x[0],desc:x[1],u:x[2],q:x[3],rate:x[4],idx:x[5],sec:x[6],mat:Math.round(x[3]*x[4]*0.42),lab:Math.round(x[3]*x[4]*0.21),eq:Math.round(x[3]*x[4]*0.22),sub:Math.round(x[3]*x[4]*0.09),cert:Math.round(x[3]*(x[5]-0.35)*0.6),rem:Math.round(x[3]*(1.35-x[5]))}));
 const secs=[
  {k:'chart',id:'bq1',ti:'Value by Section — BOQ vs Certified (₵M)',cls:'wide',spec:br(['Prelims','Earthworks','Pavement','Surfacing','Drainage','Structures'],[S('BOQ',fct([9.1,18.4,26.2,21.8,8.4,16.9],1e6),C[8]),S('Certified',fct([8.9,12.1,12.4,9.2,5.1,7.2],1e6),C[0])],{fmt:'money',height:280})},
  {k:'chart',id:'bq2',ti:'Cost Element Composition (₵M)',spec:dn([item('Materials',Math.round(41.9e6*f1),C[0]),item('Labour',Math.round(20.9e6*f1),C[1]),item('Equipment',Math.round(21.9e6*f1),C[3]),item('Subcontractors',Math.round(9.0e6*f1),C[4]),item('Overhead & margin',Math.round(10.0e6*f1),C[8])],{fmt:'money',center:'₵103.7M'})},
  {k:'chart',id:'bq3',ti:'Quantity Variance Index by Item (1.00 = on BOQ)',spec:hb(boqRows.map(b=>b.it),[S('Index',boqRows.map(b=>b.idx),C[4])],{fmt:'num'})},
  {k:'chart',id:'bq4',ti:'Profitability by Section (GP %)',spec:hb(['Prelims','Earthworks','Pavement','Surfacing','Drainage','Structures'],[S('GP %',[31,24,19,22,27,16],C[2])],{fmt:'pct'})},
  {k:'table',id:'bqt',ti:'BOQ Item Register — resource breakdown on drill',cls:'wide',cfg:{id:'bqt',page:9,search:1,cols:[{k:'it',l:'Item'},{k:'desc',l:'Description',wide:1},{k:'sec',l:'Section'},{k:'u',l:'Unit'},{k:'q',l:'BOQ qty',y:'num'},{k:'rate',l:'Rate',y:'money'},{k:'idx',l:'Qty var idx'},{k:'cert',l:'Certified qty',y:'num'},{k:'rem',l:'Remaining',y:'num'}],rows:boqRows,drill:r=>{ NWX.modal('BOQ '+r.it+' — '+r.desc,
    '<div class="statgrid">'+[['Amount',NWX.fmtM(r.q*r.rate)],['Material',NWX.fmtM(r.mat)],['Labour',NWX.fmtM(r.lab)],['Equipment',NWX.fmtM(r.eq)],['Subcontract',NWX.fmtM(r.sub)],['Certified qty',r.cert+' '+r.u],['Remaining',r.rem+' '+r.u],['Qty var index',r.idx]].map(x=>'<div class="statmini"><span>'+x[0]+'</span><b style="font-size:12.5px">'+x[1]+'</b></div>').join('')+'</div><div class="ck-body" id="bqD" style="height:220px;margin-top:10px"></div>',
    b2=>Charts.render(b2.querySelector('#bqD'),{type:'doughnut',title:'Resource split',items:[{label:'Materials',value:r.mat,color:C[0]},{label:'Labour',value:r.lab,color:C[1]},{label:'Equipment',value:r.eq,color:C[3]},{label:'Subcontract',value:r.sub,color:C[4]}],fmt:'money',center:''})); NWX.audit('BOQ item drilled',r.it); } }},
  {k:'html',id:'bqimp',ti:'BOQ Tools',cls:'wide'}
 ];
 return {title:'boq',sub:'Items · resources · measurement · variance',cards:cards,sections:secs};
};
MOUNT.bqimp=function(el){ el.innerHTML='<div class="hlist" style="display:flex;gap:8px;padding-bottom:4px;flex-wrap:wrap"><button class="btn sm" id="biImp">'+NWX.icon('download',13)+' Import BOQ (Excel/CESMM4)</button><button class="btn sm ghost" id="biTpl">Template</button><button class="btn sm ghost" id="biCmp">Compare live vs baseline</button></div><p style="margin:4px 12px 10px;font-size:11.5px;color:var(--muted)">Imports are validated column-by-column; errors block publish. Certified items are locked after first IPC.</p>';
 el.querySelector('#biImp').onclick=()=>NWX.toast('BOQ import','File parser ready — drag .xlsx: 1,248 rows mapped, 0 errors (demo). VersionV created as draft.','green');
 el.querySelector('#biCmp').onclick=()=>NWX.toast('Baseline compare','Δ vs baseline: +87 VO lines, qty variance +2.4%, rate variance −1.1% — report exported.','blue'); };

/* ============================ PLANNING & SCHEDULING ================= */
MOUNT.wbs=function(el){ let cur='P-2210'; const WBS={'P-2210':[['1 Preliminaries','92%','green',[['1.1 Mobilization','100%'],['1.2 Traffic mgmt setup','86%']]],['2 Earthworks','58%','amber',[['2.1 Cut to fill','64%'],['2.2 Rock excavation','51%'],['2.3 Haul & place','55%']]],['3 Structures','12%','blue',[['3.1 Box culverts','18%'],['3.2 Retaining walls','4%']]],['4 Pavement','0%','grey',[['4.1 Sub-base','0%'],['4.2 Base course','0%'],['4.3 Wearing course','0%']]]],'P-2201':[['1 Preliminaries','100%','green',[]],['2 Earthworks','88%','green',[['2.1 Cut to fill','92%'],['2.2 Borrow pits','80%']]],['3 Pavement','61%','amber',[['3.1 Sub-base','78%'],['3.2 Base course','52%'],['3.3 Asphalt','38%']]]]};
 function render(){ const rows=WBS[cur]||WBS['P-2210'];
  el.innerHTML='<div class="hlist" style="display:flex;gap:6px">'+Object.keys(WBS).map(p=>'<span class="chip '+(p===cur?'on':'')+'" data-p="'+p+'">'+p+'</span>').join('')+'<span style="margin-left:auto;font-size:11px;color:var(--faint)">Critical path items highlighted · baseline v3</span></div>'+
  '<div style="padding:0 12px 12px">'+rows.map(s=>'<details open class="statement" style="margin-bottom:6px"><summary style="padding:9px 12px;cursor:pointer;font-size:12.5px;font-weight:700;display:flex;gap:10px;align-items:center"><span class="badge '+s[2]+'">'+s[1]+'</span>'+s[0]+'</summary><div style="padding:2px 12px 10px 26px">'+s[3].map(a=>'<div class="hrow" style="border:none;padding:4px 0"><span class="nm" style="width:auto;flex:1">'+a[0]+'</span><div class="bar"><i style="width:'+a[1]+';background:'+(a[2]||'var(--accent)')+'"></i></div><span class="vv">'+a[1]+'</span></div>').join('')+'</div></details>').join('')+'</div>';
  el.querySelectorAll('[data-p]').forEach(ch=>ch.onclick=()=>{cur=ch.dataset.p;render();}); }
 render();};
MODULES.plan=function(M){
 const cards=[K('Active Schedules',6,{u:'#',d:0,neutral:1}),K('Milestones ≤14d',4,{u:'#',d:0}),
  K('Critical Activities',9,{u:'#',d:0}),K('Baseline Versions',14,{u:'#',d:0,neutral:1}),
  K('Weather Days Lost (MTD)',2.5,{u:'num',d:1,good:'down'}),K('EOT Requests Under Prep',2,{u:'#',d:0}),
  K('Schedule Performance Idx',0.94,{u:'num',d:2,tgt:1,good:'up',st:'amber'}),K('Slipping Milestones',3,{u:'#',good:'down',d:0})];
 const secs=[
  {k:'html',id:'wbs',ti:'Work Breakdown Structure — expandable with progress',cls:'wide'},
  {k:'chart',id:'plg',ti:'Baseline vs Current — project master Gantt',cls:'wide',spec:{type:'gantt',legend:false,fmt:'num',rows:D.PROJECTS.slice(0,6).map(p=>({name:p.id+' '+p.n.split(' ').slice(0,3).join(' '),ps:p.sd,pe:p.pd,as:p.sd,ae:p.fd,prog:p.comp,status:p.risk==='high'?'delayed':'ok'}))}},
  {k:'chart',id:'plr',ti:'Resource Loading — labour per week',spec:br(['W33','W34','W35','W36','W37','W38','W39','W40'],[S('Labour hrs',[2240,2380,2410,2360,2290,2180,2050,1960],C[1]),S('Available',[2400,2400,2400,2400,2400,2400,2400,2400],C[8])],{fmt:'num'})},
  {k:'chart',id:'pld',ti:'Delay Reasons Pareto (days, Q-to-date)',spec:hb(['Client instruction','Rainfall','Material supply','Utility relocation','Payment lag','Equipment'],[S('Days',[9,6,5,4,2,1],C[6])],{fmt:'num'})},
  {k:'chart',id:'plw',ti:'Rain Days (seasonality)',spec:br(MON,[S('Rain days',[8,9,11,13,12,7,5,4,6,8,9,7],C[1])],{fmt:'num'})},
  {k:'table',id:'plm',ti:'Milestones & EOT Register',cls:'wide',cfg:{id:'plm',page:8,cols:[{k:'a',l:'Milestone'},{k:'b',l:'Project'},{k:'c',l:'Baseline',y:'date'},{k:'d',l:'Forecast',y:'date'},{k:'e',l:'Slip (d)',y:'num'},{k:'f',l:'Critical',y:'badge'},{k:'g',l:'EOT / claim',y:'badge'}],rows:[
   {a:'Box culvert BC-04 complete',b:'P-2210',c:'2026-08-24',d:'2026-08-29',e:5,f:'red',fL:'Critical',g:'amber',gL:'EOT drafting'},
   {a:'Base course layer 1 done',b:'P-2201',c:'2026-08-19',d:'2026-08-19',e:0,f:'grey',fL:'—',g:'grey',gL:'—'},
   {a:'Girder launch span 2',b:'P-2226',c:'2026-09-08',d:'2026-09-08',e:0,f:'amber',fL:'Near-critical',g:'grey',gL:'—'},
   {a:'Surfacing 50%',b:'P-2214',c:'2026-08-15',d:'2026-08-13',e:-2,f:'grey',fL:'—',g:'green',gL:'Ahead'}]}}
 ];
 return {title:'plan',sub:'WBS · critical path · baselines · resources',cards:cards,sections:secs};
};

/* ============================ PROJECT COSTING ================= */
MODULES.costing=function(M){
 const f1=f();
 const cards=[K('Contract Revenue (YTD)',103.4e6,{u:'GHS'}),K('Certified Revenue',113.6e6,{u:'GHS'}),
  K('Cost Incurred',96.7e6,{u:'GHS',good:'down'}),K('Committed Cost',12.4e6,{u:'GHS'}),
  K('Est. Cost to Complete',89.4e6,{u:'GHS'}),K('Gross Profit YTD',6.7e6,{u:'GHS'}),
  K('Margin',6.5,{u:'%',tgt:8,d:1,st:'amber'}),K('CPI (group)',0.96,{u:'num',d:2,good:'up',st:'amber'}),
  K('SPI (group)',0.94,{u:'num',d:2,good:'up',st:'amber'}),K('Forecast FY Profit',20.1e6,{u:'GHS'}),
  K('Cost Variance',-1.8,{u:'%',good:'up',d:1,st:'red'}),K('Revenue Variance',+3.4,{u:'%',d:1})];
 const P=D.PROJECTS.map(p=>({id:p.id,bac:p.bud,ac:p.cost,ev:p.bud*p.comp/100,costF:(p.bud*0.94+ (p.risk==='high'?p.bud*0.06:0))}));
 const secs=[
  {k:'chart',id:'cs1',ti:'Cost Categories by Project (₵M)',cls:'wide',spec:br(P.map(p=>p.id),[S('Materials',P.map((p,i)=>Math.round((22+((i*7)%15))/10*10)/10),C[0]),S('Labour',P.map((p,i)=>Math.round((11+((i*5)%9))*10)/10),C[1]),S('Equipment & fuel',P.map((p,i)=>Math.round((13+((i*3)%11))*10)/10),C[3]),S('Subcontract',P.map((p,i)=>Math.round((6+((i*4)%7))*10)/10),C[4]),S('Overheads',P.map((p,i)=>Math.round((4+((i*2)%5))*10)/10),C[8])],{fmt:'num',stacked:true,height:290})},
  {k:'chart',id:'cs2',ti:'Margin Bridge — portfolio (₵M)',spec:wf([{label:'Certified revenue',value:113.6,type:'abs'},{label:'Materials',value:-42.4,type:'delta'},{label:'Labour',value:-21.2,type:'delta'},{label:'Plant & fuel',value:-19.6,type:'delta'},{label:'Subcontracts',value:-13.1,type:'delta'},{label:'Site overhead',value:-10.6,type:'delta'},{label:'Gross profit',value:6.7,type:'total'}],{fmt:'num',height:280})},
  {k:'chart',id:'cs3',ti:'EAC vs BAC by Project (₵M)',spec:br(P.map(p=>p.id),[S('BAC',P.map(p=>Math.round(p.bac/1e5)/10),C[8]),S('EAC',P.map(p=>Math.round(p.costF/1e5)/10),C[6])],{fmt:'num'})},
  {k:'chart',id:'cs4',ti:'CPI / SPI Trend',spec:ln(MON,[S('CPI',ts(12,0.97,.03,.002,'cpi'),C[0]),S('SPI',ts(12,0.95,.03,.004,'spi'),C[1]),S('Target',Array(12).fill(1),C[6],{dash:true})],{fmt:'num',min:0.85,max:1.06})},
  {k:'table',id:'cst',ti:'Earned-Value Workbench',cls:'wide',cfg:{id:'cst',page:9,cols:[{k:'id',l:'Project'},{k:'bac',l:'BAC',y:'money'},{k:'ac',l:'Actual cost',y:'money'},{k:'ev',l:'Earned value',y:'money'},{k:'cpi',l:'CPI'},{k:'spi',l:'SPI'},{k:'etc',l:'ETC',y:'money'},{k:'eac',l:'EAC',y:'money'},{k:'vac',l:'VAC',y:'money'},{k:'j',l:'Health',y:'badge'}],rows:P.map(p=>{const cpi=Math.round(p.ev/p.ac*100)/100,spi=Math.round((p.ev)/(p.bac*(p.comp/100>0.5?0.7:0.45))*100)/100,etc=Math.max(0,p.bac-p.ev)/(cpi||1),eac=p.ac+etc,vac=p.bac-eac;
   return {id:p.id,bac:p.bac,ac:p.ac,ev:Math.round(p.ev),cpi:cpi.toFixed(2),spi:spi.toFixed(2),etc:Math.round(etc),eac:Math.round(eac),vac:Math.round(vac),j:vac<0?'red':eac>p.bac*0.98?'amber':'green',jL:vac<0?'Overrun':eac>p.bac*0.98?'Watch':'On plan'};}),totals:['bac','ac','ev','etc','eac']}}
 ];
 return {title:'costing',sub:'EVM · EAC · margin bridge · variances',cards:cards,sections:secs};
};

/* ============================ TENDERS ============================ */
MODULES.tenders=function(M){
 const f1=f();
 const cards=[K('Pipeline Value',23.7e6,{u:'GHS',neutral:1}),K('Live Tenders',4,{u:'#',d:0}),
  K('Submitted Awaiting Result',2,{u:'#',d:0,neutral:1}),K('Won YTD',9,{u:'#',d:0}),
  K('Win Rate',38,{u:'%',tgt:35}),K('Avg Bid Margin',14.2,{u:'%',d:1}),K('Closing < 7 days',2,{u:'#',d:0,good:'down'}),
  K('Bid Securities Out',3,{u:'#',d:0}),K('Lessons Logged',24,{u:'#',d:0,neutral:1})];
 const secs=[
  {k:'chart',id:'tdf',ti:'Bid Funnel (12 months)',cls:'w8',spec:fnl([item('Opportunities logged',38,C[8]),item('Bid/no-bid approved',26,C[1]),item('Priced & submitted',24,C[0]),item('Shortlisted',15,C[3]),item('Won',9,C[2])],{fmt:'num',height:280})},
  {k:'chart',id:'tds',ti:'Pipeline by Source (₵M)',spec:dn([item('Public tenders',Math.round(12.9e6*f1),C[0]),item('Direct invites',Math.round(6.1e6*f1),C[1]),item('Prequalified lists',Math.round(3.8e6*f1),C[3]),item('Framework call-offs',Math.round(0.9e6*f1),C[4])],{fmt:'money',center:'₵23.7M'})},
  {k:'chart',id:'tdw',ti:'Won vs Lost (by quarter)',spec:br(['Q4-25','Q1-26','Q2-26','Q3-26'],[S('Won',[2,3,3,1],C[2]),S('Lost',[3,2,4,2],C[6])],{fmt:'num'})},
  {k:'chart',id:'tda',ti:'Estimate Accuracy — actual/estimate cost (%)',spec:ln(MON,[S('%',ts(12,97.6,.03,.002,'tda'),C[4],{area:true})],{fmt:'pct',min:90,max:104,target:{value:100,label:'100'}})},
  {k:'table',id:'tdt',ti:'Tender Register',cls:'wide',cfg:{id:'tdt',page:8,search:1,cols:[{k:'no',l:'Tender'},{k:'name',l:'Name',wide:1},{k:'cl',l:'Client'},{k:'src',l:'Source'},{k:'close',l:'Closing',y:'date'},{k:'est',l:'Estimate',y:'money'},{k:'stat',l:'Stage / status'},{k:'res',l:'Result',y:'badge',badge:v=>!v||v==='—'?'grey':v.includes('Won')?'green':v.includes('Lost')?'red':'blue'}],rows:D3.TENDERS,drill:r=>NWX.modal(r.no+' — lessons learned','<p style="font-size:13px">'+(r.ll&&r.ll!=='—'?r.ll:'No post-bid review yet — will be captured when the result is published.')+'</p><p style="font-size:12px;color:var(--muted)">Estimating guardrails updated feed future bids automatically (rates library v26.8).</p>')}}
 ];
 return {title:'tenders',sub:'Pipeline · pricing · results · lessons',cards:cards,sections:secs};
};

/* ============================ MRP ================= */
MOUNT.mrp=function(el){ const lines=mrpLines();
 el.innerHTML='<div class="hlist" style="padding-bottom:4px"><span class="chip on">Firming window 14 days</span><span class="chip">Lead-time buffer 3d</span><span class="chip">Waste allowance 4%</span></div>'+
 '<div class="tbl-wrap"><table class="tbl"><thead><tr><th>Project</th><th>Phase / activity</th><th>Material</th><th class="num">Net need</th><th class="num">Stock</th><th class="num">Reserved</th><th class="num">On order</th><th class="num">Shortfall</th><th>Suggested action</th><th>By</th></tr></thead><tbody>'+
 lines.map((l,i)=>'<tr data-i="'+i+'" style="cursor:pointer"><td>'+l.prj+'</td><td>'+l.act+'</td><td>'+l.mat+'</td><td class="num">'+l.need+'</td><td class="num">'+l.stock+'</td><td class="num">'+l.rsv+'</td><td class="num">'+l.ono+'</td><td class="num">'+(l.short>0?'<b style="color:var(--red)">'+l.short+'</b>':'0')+'</td><td><span class="badge '+(l.actn.includes('Purchase')?'amber':l.actn.includes('Transfer')?'blue':'green')+'">'+l.actn+'</span></td><td>'+l.by+'</td></tr>').join('')+'</tbody></table></div>';
 el.querySelectorAll('tr[data-i]').forEach(r=>r.onclick=()=>{ const l=lines[+r.dataset.i];
  NWX.modal('Material requirement — '+l.mat,'<div class="statgrid">'+[['Project',l.prj],['Activity',l.act],['Net requirement',l.need],['In stock',l.stock],['Reserved',l.rsv],['On open PO',l.ono],['Shortfall',Math.max(0,l.short)],['Suggested',l.actn],['Need by',l.by]].map(x=>'<div class="statmini"><span>'+x[0]+'</span><b style="font-size:12.5px">'+x[1]+'</b></div>').join('')+'</div><div style="display:flex;gap:8px;margin-top:12px"><button class="btn sm pri" id="mGen">Generate document</button><button class="btn sm ghost" id="mAll">Allocate from stockpile</button></div>',
  b2=>{ b2.querySelector('#mGen').onclick=()=>{ NWX.quickForm(l.actn.includes('Purchase')?'Purchase Requisition':'Issue Stock'); b2.closest('.modal-ov').remove(); };
   b2.querySelector('#mAll').onclick=()=>{ NWX.toast('Reserved','Stock at Shai Hills pile A2 reserved to '+l.prj+' until '+l.by+'.','green'); b2.closest('.modal-ov').remove(); }; }); }); };
function mrpLines(){return [{prj:'P-2201',act:'Base course — km 38–44',mat:'Base course GS',need:'24,000 t',stock:'18,400 t',rsv:'6,000',ono:'12,000',short:0,actn:'Quarry issue request',by:'2026-08-18'},
 {prj:'P-2201',act:'Wearing course',mat:'Aggregate 14mm',need:'9,600 t',stock:'11,250 t',rsv:'2,000',ono:'—',short:0,actn:'Quarry issue request',by:'2026-09-02'},
 {prj:'P-2210',act:'Box culverts BC-02/03/04',mat:'Rebar H16',need:'86 t',stock:'7 t',rsv:'5',ono:'40',short:44,actn:'Purchase requisition',by:'2026-08-15'},
 {prj:'P-2210',act:'Box culverts',mat:'Cement 42.5R',need:'1,900 bags',stock:'220',rsv:'0',ono:'800',short:1080,actn:'Purchase requisition',by:'2026-08-14'},
 {prj:'P-2221',act:'Sub-grade improvement',mat:'Crusher run',need:'4,200 t',stock:'3,310 t',rsv:'—',ono:'—',short:890,actn:'Transfer from Shai Hills',by:'2026-08-22'},
 {prj:'P-2226',act:'Slope protection',mat:'Riprap 200–500kg',need:'1,150 pc',stock:'740',rsv:'—',ono:'—',short:410,actn:'Quarry production order',by:'2026-08-26'}];}
MODULES.mrp=function(M){
 const cards=[K('Requirement Lines',118,{u:'#',d:0,neutral:1}),K('Shortfall Lines',3,{u:'#',good:'down',d:0}),
  K('PRs Auto-Suggested',6,{u:'#',d:0}),K('Transfers Suggested',2,{u:'#',d:0}),K('Quarry Issues Suggested',9,{u:'#',d:0}),
  K('PO Coverage',87,{u:'%',tgt:90}),K('Expediting Watchlist',4,{u:'#',good:'down',d:0}),K('Material-Service Level',96.4,{u:'%',tgt:97,d:1})];
 const secs=[
  {k:'html',id:'mrp',ti:'Net-Requirement Workbench — click a line to act',cls:'wide',live:1},
  {k:'chart',id:'mr1',ti:'Net Requirement by Material (next 4 wks)',spec:hb(['Base course','14mm agg.','Rebar','Cement','Crusher run','Riprap','Diesel','Geotextile'],[S('t / units',[24,9.6,86,1900,4.2,1150,118,2.4],C[0])],{fmt:'num'})},
  {k:'chart',id:'mr2',ti:'Requirement vs Coverage (%)',spec:br(DAYS30.slice(-14).slice(0,7),[S('Covered %',[88,90,87,92,91,93,96],C[2]),S('Required',Array(7).fill(100),C[8],{dash:true})],{fmt:'pct'})},
  {k:'chart',id:'mr3',ti:'Shortfalls by Week',spec:br(['W33','W34','W35','W36'],[S('Lines short',[4,3,6,2],C[6])],{fmt:'num'})}
 ];
 return {title:'mrp',sub:'BOQ × schedule × stock × POs → action',cards:cards,sections:secs};
};

/* ============================ PAYROLL ================= */
MODULES.payroll=function(M){
 const rows=D3.payrollRows();
 const sum=k=>rows.reduce((a,r)=>a+r[k],0);
 const cards=[K('Gross Payroll (Aug run)',sum('gross')*50,{u:'GHS',tgt:4.1e6}),K('Net Pay',sum('net')*50,{u:'GHS'}),
  K('Headcount Paid',498,{u:'#',d:0}),K('PAYE',sum('paye')*50,{u:'GHS'}),K('SSNIT (EE+ER)',(sum('ssEE')+sum('ssER'))*50,{u:'GHS'}),
  K('Tier 2 Pension',sum('t2')*50,{u:'GHS'}),K('Loans Recovered',sum('loan')*50,{u:'GHS',good:'down'}),
  K('MoMo Payroll Share',14,{u:'%',d:0}),K('Overtime Cost',sum('ot')*50,{u:'GHS',good:'down'}),
  K('Run Status','Computed',{u:'text',st:'blue'})];
 const secs=[
  {k:'chart',id:'py1',ti:'Cost Breakdown Trend (₵M/month)',cls:'wide',spec:br(MON.slice(0,8),[S('Basic',fct(ts(8,2.9e6,.02,.006,'py1'),f()),C[0]),S('Overtime',fct(ts(8,0.42e6,.18,0,'py2'),f()),C[4]),S('Allowances',fct(ts(8,0.38e6,.08,0,'py3'),f()),C[3]),S('Statutory (ER)',fct(ts(8,0.52e6,.03,.005,'py4'),f()),C[8])],{fmt:'money',stacked:true,height:280})},
  {k:'chart',id:'py2',ti:'Deductions & Statutory (₵/month)',spec:ln(MON,[S('PAYE',fct(ts(12,498e3,.04,.008,'py5'),f()),C[6]),S('SSNIT EE',fct(ts(12,161e3,.04,.008,'py6'),f()),C[1]),S('Tier 2',fct(ts(12,146e3,.04,.008,'py7'),f()),C[3])],{fmt:'money'})},
  {k:'chart',id:'py3',ti:'Payroll by Category (₵)',spec:hb(['Quarry workers','Plant operators','Drivers','Mechanics','Engineers','Surveyors/Geologists','Blasters','Safety officers','Project staff','Office staff'],[S('₵',fct([680,420,390,310,520,280,240,96,690,364],1e3),C[0])],{fmt:'money'})},
  {k:'chart',id:'py4',ti:'Quarry vs Project Allocation (₵M)',spec:dn([item('Quarry allocation',Math.round(1.62e6*f()),C[0]),item('Project allocation',Math.round(1.84e6*f()),C[1]),item('Head office',Math.round(0.46e6*f()),C[8])],{fmt:'money',center:'Split'})},
  {k:'table',id:'pyt',ti:'Payroll Register — August 2026 (sample)',cls:'wide',cfg:{id:'pyt',page:10,search:1,cols:[{k:'id',l:'ID'},{k:'name',l:'Employee'},{k:'cat',l:'Category'},{k:'basic',l:'Basic',y:'money'},{k:'ot',l:'OT',y:'money'},{k:'allow',l:'Allowances',y:'money'},{k:'gross',l:'Gross',y:'money'},{k:'paye',l:'PAYE',y:'money'},{k:'ssEE',l:'SSNIT 5.5%',y:'money'},{k:'t2',l:'Tier 2 5%',y:'money'},{k:'loan',l:'Loan',y:'money'},{k:'net',l:'Net pay',y:'money'},{k:'mode',l:'Pay via'},{k:'st',l:'Status',y:'badge',badge:v=>v==='Computed'?'blue':'amber'}],rows:rows,totals:['basic','gross','net','paye'],drill:r=>payslip(r)}}
 ];
 return {title:'payroll',sub:'Earnings · PAYE/SSNIT/Tier-2 · MoMo & bank file',cards:cards,sections:secs};
};
function payslip(r){ const html=printPay(r); NWX.modal('Payslip — '+r.name,'<div style="max-height:56vh;overflow:auto">'+html+'</div><div style="display:flex;gap:8px;margin-top:10px"><button class="btn sm" id="psP">'+NWX.icon('print',13)+' Print / PDF</button><button class="btn sm" id="psM">'+NWX.icon('mail',13)+' Email to employee</button></div>',
 b2=>{ b2.querySelector('#psP').onclick=()=>NWX.printHTML('Payslip — '+r.name,html);
  b2.querySelector('#psM').onclick=()=>NWX.toast('Emailed','Password-protected payslip sent (DMS copy retained).','green'); NWX.audit('Payslip viewed',r.id);});}
function printPay(r){ const rows2=[['Basic salary',r.basic],['Overtime ('+(r.ot/72|0)+' h)',r.ot],['Risk + site + transport allowance',r.allow],['<b>Gross pay</b>','<b>'+r.gross.toLocaleString()+'</b>'],['PAYE (GRA bands)',-r.paye],['SSNIT employee 5.5%',-r.ssEE],['Tier 2 pension 5%',-r.t2],['Loan recovery',-r.loan],['<b>Net pay</b>','<b>'+r.net.toLocaleString()+'</b>']];
 return '<table style="width:100%;border-collapse:collapse;font-size:12.5px"><caption style="text-align:left;padding-bottom:8px"><b>NOWILLS SQC — Payslip August 2026</b><br>'+r.id+' · '+r.name+' · '+r.cat+'</caption>'+rows2.map(x=>'<tr><td style="border-bottom:1px solid #8882;padding:6px">'+x[0]+'</td><td style="border-bottom:1px solid #8882;padding:6px;text-align:right">₵ '+(''+x[1]).toLocaleString()+'</td></tr>').join('')+'</table><p style="font-size:10px;color:#666">Employer SSNIT 13%: ₵'+r.ssER.toLocaleString()+' also remitted · tax-grade G'+r.grade+' · paid via '+r.mode+'</p>';}

/* ============================ FIXED ASSETS ================= */
MODULES.fixedassets=function(M){
 const A=D3.ASSETS;const sum=k=>A.reduce((a,x)=>a+x[k],0);
 const cards=[K('Gross Book Value',sum('cost')*10,{u:'GHS',neutral:1}),K('Accumulated Depreciation',sum('accdep')*10,{u:'GHS',good:'down'}),
  K('Net Book Value',sum('nbv')*10,{u:'GHS'}),K('Additions YTD',11.8e6,{u:'GHS'}),K('Depreciation (MTD)',1.9e6,{u:'GHS',good:'down'}),
  K('Disposals YTD',0.8e6,{u:'GHS'}),K('Insured Value',58.2e6,{u:'GHS'}),K('Verification Due (30d)',64,{u:'#',d:0,good:'down'}),
  K('Impairment Review Open',1,{u:'#',d:0,good:'down'})];
 const secs=[
  {k:'chart',id:'fa1',ti:'NBV by Asset Class (₵M)',spec:hb(A.map(a=>a.cls),[S('NBV',A.map(a=>Math.round(a.nbv/1e5)/10),C[0])],{fmt:'num'})},
  {k:'chart',id:'fa2',ti:'Composite NBV Waterfall (₵M)',spec:wf([{label:'NBV 31 Dec 25',value:78.4,type:'abs'},{label:'Additions',value:11.8,type:'delta'},{label:'Depreciation',value:-4.2,type:'delta'},{label:'Disposals NBV',value:-0.8,type:'delta'},{label:'Revaluation',value:0.9,type:'delta'},{label:'NBV today',value:86.1,type:'total'}],{fmt:'num'})},
  {k:'chart',id:'fa3',ti:'Depreciation Trend (₵/month)',spec:ar(MON,[S('₵',fct(ts(12,1.8e6,.04,.006,'fa3'),f()),C[4],{area:true})],{fmt:'money'})},
  {k:'chart',id:'fa4',ti:'Capex vs Depreciation (₵M)',spec:br(MON.slice(0,8),[S('Capex',fct(ts(8,2.2e6,.4,0,'fa4'),f()),C[2]),S('Depreciation',fct(ts(8,1.9e6,.04,0,'fa5'),f()),C[6])],{fmt:'money'})},
  {k:'table',id:'fat',ti:'Asset Register (classes & totals)',cls:'wide',cfg:{id:'fat',page:9,search:1,cols:[{k:'code',l:'Code'},{k:'cls',l:'Class'},{k:'desc',l:'Composition',wide:1},{k:'loc',l:'Location'},{k:'cost',l:'Cost',y:'money'},{k:'rate',l:'Rate',y:'pct'},{k:'accdep',l:'Acc. dep',y:'money'},{k:'nbv',l:'NBV',y:'money'},{k:'cond',l:'Condition'},{k:'ins',l:'Insurance',y:'badge',badge:v=>'green'}],rows:A.map(a=>Object.assign({},a,{rate:Math.round(a.rate*100)+'',insL:'Active'})),totals:['cost','accdep','nbv'],
   drill:r=>NWX.modal(r.code+' — '+r.cls,'<div class="statgrid">'+[['Cost',NWX.fmtM(r.cost)],['Rate',r.rate+'%'],['Acc. depreciation',NWX.fmtM(r.accdep)],['NBV',NWX.fmtM(r.nbv)],['Location',r.loc],['Condition',r.cond],['Last verification',r.ver],['Insurance',r.ins]].map(x=>'<div class="statmini"><span>'+x[0]+'</span><b style="font-size:12px">'+x[1]+'</b></div>').join('')+'</div><p style="font-size:11.5px;color:var(--muted)">QR-tagged · maintenance integration live · auction/disposal via FA workflow with DoA approvals.</p>') }}
 ];
 return {title:'fixedassets',sub:'Register · depreciation · verification · insurance',cards:cards,sections:secs};
};

/* ============================ LOANS & EQUIPMENT FINANCE ================= */
MODULES.loans=function(M){
 const L=D3.LOANS;
 const cards=[K('Active Facilities',5,{u:'#',d:0,neutral:1}),K('Principal Outstanding',18.4e6,{u:'GHS',good:'down'}),
  K('New Facilities (YTD)',1,{u:'#',d:0}),K('Interest Cost (MTD)',212e3,{u:'GHS',good:'down'}),
  K('Weighted Avg Rate',26.1,{u:'%',d:1,good:'down'}),K('Next-30d Installments',1.29e6,{u:'GHS'}),
  K('Covenants Met',4,{u:'#',d:0}),K('Covenant Watch',1,{u:'#',good:'down',d:0}),K('Headroom (undrawn)',6.2e6,{u:'GHS',neutral:1})];
 const secs=[
  {k:'chart',id:'ln1',ti:'Principal Balance Amortization (₵M)',cls:'wide',spec:ar(MON.concat(['Sep','Oct','Nov','Dec']),[S('Balance',fct([21.2,20.8,20.4,20.0,19.6,19.2,18.8,18.4,17.9,17.4,16.9,16.5,16.1,15.7,15.3,14.9],1e6),C[0],{area:true})],{fmt:'money',height:280})},
  {k:'chart',id:'ln2',ti:'Balance by Lender (₵M)',spec:dn([item('GCB Bank',9.1,C[0]),item('Stanbic',2.9,C[1]),item('Ecobank',1.2,C[3]),item('CalBank',2.2,C[4]),item('Hire-purchase (DFC)',3.0,C[5])],{fmt:'num',center:'₵18.4M'})},
  {k:'chart',id:'ln3',ti:'Rate by Facility (%)',spec:hb(L.map(l=>l.no),[S('%',L.map(l=>Math.round(l.rate*1000)/10),C[6])],{fmt:'pct'})},
  {k:'chart',id:'ln4',ti:'Debt Service Coverage (×)',spec:ln(MON,[S('DSCR',ts(12,2.1,.05,.01,'dscr'),C[2])],{fmt:'num',min:1.2,max:2.8,target:{value:1.5,label:'covenant'}})},
  {k:'table',id:'lnt',ti:'Facilities Register — click for amortization schedule',cls:'wide',cfg:{id:'lnt',page:8,cols:[{k:'no',l:'Facility'},{k:'name',l:'Purpose',wide:1},{k:'orig',l:'Original',y:'money'},{k:'bal',l:'Balance',y:'money'},{k:'rate',l:'Rate',y:'pct'},{k:'inst',l:'Installment'},{k:'next',l:'Next due',y:'date'},{k:'sec',l:'Security'},{k:'cov',l:'Covenant',y:'badge',badge:v=>v==='Met'?'green':'amber'}],rows:L,drill:r=>amort(r)}}
 ];
 return {title:'loans',sub:'Facilities · amortization · covenants · security',cards:cards,sections:secs};
};
function amort(l){ const P=l.bal,r=l.rate/12,n=18; const pmt=P*r/(1-Math.pow(1+r,-n));
 let bal=P,rows='<table class="tbl"><thead><tr><th>#</th><th>Installment</th><th class="num">Interest</th><th class="num">Principal</th><th class="num">Balance</th></tr></thead><tbody>';
 for(let i=1;i<=12;i++){const it=bal*r,pr=pmt-it;bal-=pr;rows+='<tr><td>'+i+'</td><td>'+new Date(2026,7+i,20).toISOString().slice(0,10)+'</td><td class="num">'+NWX.fmtM(it)+'</td><td class="num">'+NWX.fmtM(pr)+'</td><td class="num">'+NWX.fmtM(Math.max(0,bal))+'</td></tr>';}
 NWX.modal('Amortization — '+l.no,rows+'</tbody></table><p style="font-size:11px;color:var(--muted)">Indicative schedule from current balance at '+(l.rate*100).toFixed(2)+'% p.a. · installment '+NWX.fmtM(pmt)+'/mo · 12 of '+(n)+' shown.</p>'); NWX.audit('Loan schedule viewed',l.no);}

/* ============================ REHABILITATION ================= */
MODULES.rehab=function(M){
 const f1=f();
 const cards=[K('Mined-out Area',155,{u:'num',info:'ha cumulative'}),K('Rehabilitated',59,{u:'num',info:'ha'}),
  K('In Progress',10,{u:'num',info:'ha'}),K('Closure Provision',2.96e6,{u:'GHS'}),
  K('Trees Planted (YTD)',4820,{u:'#'}),K('Compliance Score',94,{u:'%',tgt:95}),
  K('Handovers Completed',2,{u:'#',d:0,neutral:1}),K('Post-Closure Inspections Due',1,{u:'#',d:0})];
 const secs=[
  {k:'chart',id:'rh1',ti:'Cumulative Rehabilitation vs Plan (ha)',cls:'wide',spec:ln(MON,[S('Actual',fct(ts(12,4.9,.08,.07,'rh1'),f1),C[2],{area:true}),S('Plan',fct(ts(12,4.6,.02,.08,'rh2'),f1),C[8],{dash:true})],{fmt:'num',height:270})},
  {k:'chart',id:'rh2',ti:'Provision vs Estimated Liability (₵M)',spec:br(['Shai Hills','Buoho','Nsawam'],[S('Provision',fct([1.66,0.86,0.44],1e6),C[0]),S('Estimated liability',fct([1.9,0.98,0.52],1e6),C[6])],{fmt:'money'})},
  {k:'chart',id:'rh3',ti:'Stage Distribution (ha)',spec:dn([item('Backfilled',26,C[8]),item('Slope stabilised',14,C[1]),item('Topsoil replaced',9,C[4]),item('Revegetated',7,C[2]),item('Handed over',3,C[5])],{fmt:'num',center:'ha'})},
  {k:'table',id:'rht',ti:'Rehabilitation Parcels',cls:'wide',cfg:{id:'rht',page:8,cols:[{k:'id',l:'Parcel'},{k:'area',l:'Area'},{k:'ha',l:'Size (ha)',y:'num'},{k:'stage',l:'Stage'},{k:'veg',l:'Vegetation / note',wide:1},{k:'due',l:'Target',y:'date'},{k:'prov',l:'Provision (₵k)',y:'num'}],rows:D3.REHAB,totals:['ha','prov']}}
 ];
 return {title:'rehab',sub:'Progressive rehab · closure provision · handover',cards:cards,sections:secs};
};

/* ============================ QC & LABORATORY ================= */
MODULES.qc=function(M){
 const t=D3.qcTests(); const pass=t.filter(x=>x.pass).length;
 const cards=[K('Tests Today',14,{u:'#',d:0}),K('Pass Rate (MTD)',96.8,{u:'%',tgt:96,d:1}),
  K('Certificates Issued',124,{u:'#',d:0}),K('Batches On Hold',2,{u:'#',good:'down',d:0}),
  K('Rejected Batches',1,{u:'#',good:'down',d:0}),K('Calibrations Due',3,{u:'#',good:'down',d:0}),
  K('Concrete Cubes (28d pass)',98.6,{u:'%',tgt:97,d:1}),K('Field Density Pass',97.2,{u:'%',tgt:97,d:1}),
  K('Open NCRs',4,{u:'#',good:'down',d:0})];
 const secs=[
  {k:'chart',id:'qc1',ti:'LAA Control Chart — with UCL/LCL (target ≤30)',cls:'wide',spec:ln(DAYS30.slice(-14),[S('LAA result',ts(14,22,.12,0,'qc1'),C[0]),S('UCL 28',Array(14).fill(28),C[6],{dash:true}),S('Nominal 22',Array(14).fill(22),C[8],{dash:true})],{fmt:'num',min:10,max:34,height:280})},
  {k:'chart',id:'qc2',ti:'Sieve Curve — 20mm product (% passing)',spec:ln(['31.5','25','20','14','10','5','2.36','0.075'],[S('This batch',[100,98,84,52,24,9,4,1],C[0]),S('Spec upper',[100,100,90,58,32,14,7,2],C[2],{dash:true}),S('Spec lower',[100,95,72,40,16,5,1,0],C[6],{dash:true})],{fmt:'pct'})},
  {k:'chart',id:'qc3',ti:'Tests by Type (MTD)',spec:hb(['Sieve analysis','ACV','AIV','Flakiness','Moisture','LAA','Concrete cubes','Field density','Slump','Soil compaction'],[S('Count',[46,18,14,16,62,12,88,34,90,22],C[1])],{fmt:'num'})},
  {k:'chart',id:'qc4',ti:'Pass Rate by Lab (%)',spec:br(['Quarry lab SH','Site lab P-2201','Site lab P-2210','3rd-party check'],[S('%',[97.4,96.2,95.8,99.1],C[2])],{fmt:'pct',min:90,max:100})},
  {k:'chart',id:'qc5',ti:'Moisture Content Trend (%)',spec:ar(DAYS30.slice(-14),[S('%',ts(14,0.9,.2,0,'qc5'),C[8],{area:true})],{fmt:'pct',min:0,max:2})},
  {k:'table',id:'qct',ti:'Test Register — certificates & chain of custody',cls:'wide',cfg:{id:'qct',page:10,search:1,cols:[{k:'id',l:'Test'},{k:'test',l:'Method'},{k:'sample',l:'Sample / product',wide:1},{k:'spec',l:'Spec limit'},{k:'result',l:'Result'},{k:'pass',l:'Verdict',y:'badge',badge:v=>v?'green':'red'},{k:'by',l:'Lab'},{k:'date',l:'Date',y:'date'},{k:'cert',l:'Certificate'},{k:'stat',l:'Batch',y:'badge',badge:v=>v==='Approved'||v==='Released'?'green':'amber'}],rows:t.map(x=>Object.assign({},x,{passL:x.pass?'Pass':'FAIL',result:x.result==null?'grading':x.result+' '+x.unit}))}}
 ];
 return {title:'qc',sub:'Lab tests · certificates · control charts · NCRs',cards:cards,sections:secs};
};

/* ============================ SECURITY & GATE ================= */
MOUNT.gatelive=function(el){ let inN=214,outN=196,hold=1;
 function render(){ el.innerHTML='<div class="statgrid" style="padding:12px">'+
  [['Gate A — IN',inN,'green'],['Gate A/B — OUT',outN,'green'],['Visitors today','38','blue'],['Blacklist holds',hold,'red'],['ANPR match','99.4%','green'],['Unauthorized-removal alerts','0','green']].map(x=>'<div class="statmini"><span>'+x[0]+'</span><b style="color:var(--'+x[2]+')">'+x[1]+'</b></div>').join('')+'</div>'+
  '<div style="padding:0 14px 12px;font-size:11.5px;color:var(--muted)">CCTV feeds linked · ANPR enforced · every OUT vehicle needs a matched weighbridge ticket + gate pass.</div>'; }
 render(); NWX.addLive(()=>{ if(Math.random()<0.5){inN++;} if(Math.random()<0.45){outN++;} render(); });};
MODULES.gate=function(M){
 const f1=f(); const rows=D3.gateLog();
 const cards=[K('Entries Today',214,{u:'#',d:0,neutral:1}),K('Exits Today',196,{u:'#',d:0}),
  K('Visitors Registered',38,{u:'#',d:0}),K('Gate Passes Issued (OUT)',96,{u:'#',d:0}),
  K('Blacklist Holds',1,{u:'#',good:'down',d:0}),K('ANPR Match Rate',99.4,{u:'%',d:1}),
  K('Security Incidents (wk)',2,{u:'#',good:'down',d:0}),K('CCTV Uptime',99.8,{u:'%',st:'green',d:1})];
 const secs=[
  {k:'html',id:'gatelive',ti:'Gatehouse Live Counters',cls:'wide',live:1},
  {k:'chart',id:'gt1',ti:'Traffic by Hour (IN/OUT)',spec:br(HRS.slice(5,20),[S('IN',fct(ts(15,16,.3,.02,'gt1'),f1),C[1]),S('OUT',fct(ts(15,15,.3,.02,'gt2'),f1),C[4])],{fmt:'num'})},
  {k:'chart',id:'gt2',ti:'Visitor Categories (wk)',spec:dn([item('Customers',42,C[0]),item('Suppliers',31,C[1]),item('Contractors',18,C[3]),item('Regulators',6,C[4]),item('Other',9,C[8])],{fmt:'num',center:'Wk'})},
  {k:'chart',id:'gt3',ti:'Security Incidents Trend',spec:ln(MON.slice(0,8),[S('Incidents',ts(8,2.6,.4,-.06,'gt3'),C[6])],{fmt:'num',min:0})},
  {k:'table',id:'gtt',ti:'Gate Log — ANPR verified',cls:'wide',cfg:{id:'gtt',page:10,search:1,cols:[{k:'time',l:'Time'},{k:'gate',l:'Gate'},{k:'dir',l:'Dir',y:'badge',badge:v=>v==='IN'?'blue':'green'},{k:'veh',l:'Vehicle'},{k:'who',l:'Driver / visitor'},{k:'what',l:'Load / purpose',wide:1},{k:'tick',l:'WB ticket'},{k:'anpr',l:'ANPR',y:'badge',badge:v=>'green'},{k:'auth',l:'Authorization'},{k:'st',l:'Status',y:'badge',badge:v=>v.includes('Held')?'red':'green'}],rows:rows}}
 ];
 return {title:'gate',sub:'ANPR · gate passes · blacklists · incidents',cards:cards,sections:secs};
};

/* ============================ OFFLINE & SYNC ================= */
MOUNT.syncboard=function(el){
 function render(){ const N=D3.SYNC_NODES;
  el.innerHTML='<div class="crusher-grid">'+N.map((n,i)=>'<div class="crusher"><h4><span class="led '+(n.st.startsWith('Offline')?'r':n.conf?'a':'g')+'"></span>'+n.loc+'</h4>'+
  '<div class="row"><span>Node ID</span><b class="mono">'+n.id+'</b></div><div class="row"><span>Last sync</span><b>'+n.last+'</b></div>'+
  '<div class="row"><span>Uploaded</span><b>'+n.up+'</b></div><div class="row"><span>Queued offline</span><b>'+(Math.max(0,n.queued + (window.__qadd||0)))+'</b></div>'+
  '<div class="row"><span>Conflicts</span><b>'+n.conf+'</b></div><div class="row"><span>Status</span><b>'+n.st+'</b></div>'+
  '<div style="margin-top:6px"><button class="btn sm '+(n.st.startsWith('Offline')?'pri':'ghost')+'" data-s="'+i+'">'+(n.st.startsWith('Offline')?'Force sync + retry':'Sync now')+'</button>'+(n.conf?'<button class="btn sm" data-c="'+i+'">Resolve conflicts</button>':'')+'</div></div>').join('')+'</div>';
  el.querySelectorAll('[data-s]').forEach(b=>b.onclick=()=>{ const n=N[+b.dataset.s]; NWX.toast('Sync started','Node '+n.id+' — encrypted session, delta sync, dedupe hash-chain…','blue');
   setTimeout(()=>{ n.queued=0;n.up+=14;n.last='just now'; if(Math.random()>0.3&&n.st.startsWith('Offline'))n.st='Recovered · verifying'; NWX.toast('Sync complete','Node '+n.id+' — '+n.up+' records uploaded, 0 duplicates, receipts acked.','green'); NWX.audit('Sync executed',n.id); render(); },1200); });
  el.querySelectorAll('[data-c]').forEach(b=>b.onclick=()=>NWX.modal('Conflict resolution — Buoho node','<table class="tbl"><thead><tr><th>Record</th><th>Local value</th><th>Server value</th><th>Winner</th></tr></thead><tbody><tr><td>WB-9184 net tonnes</td><td>26.4 t (offline 08:16)</td><td>26.9 t (corrected 09:02)</td><td><select class="inline-edit"><option>Server</option><option>Local</option></select></td></tr></tbody></table><p style="font-size:11px;color:var(--muted)">Loser preserved in conflict archive · both timestamps kept · supervisor resolution required per policy.</p>',null,'Apply resolution')); }
 render();};
MODULES.sync=function(M){
 const cards=[K('Registered Nodes',7,{u:'#',d:0,neutral:1}),K('Online Now',6,{u:'#',d:0}),
  K('Queued Records',12,{u:'#',good:'down',d:0}),K('Conflicts Pending',1,{u:'#',good:'down',d:0}),
  K('Avg Sync Lag','4.2 s',{u:'text',st:'green'}),K('Failed Retries (24h)',2,{u:'#',good:'down',d:0}),
  K('Encryption','AES-256',{u:'text',st:'green'}),K('Coverage — offline forms',12,{u:'#',d:0,neutral:1})];
 const secs=[
  {k:'html',id:'syncboard',ti:'Node Fleet — force sync, resolve conflicts',cls:'wide',live:1},
  {k:'chart',id:'sy1',ti:'Sync Volume (records/day)',spec:ar(DAYS30.slice(-14),[S('Uploaded',ts(14,2640,.18,.01,'sy1'),C[0],{area:true})],{fmt:'num'})},
  {k:'chart',id:'sy2',ti:'Conflict Rate (per 1k records)',spec:ln(DAYS30.slice(-14),[S('Rate',ts(14,0.4,.5,-.02,'sy2'),C[6])],{fmt:'num',min:0,max:1.5})},
  {k:'html',id:'mobapps',ti:'Mobile Applications — offline-first by design',cls:'wide'}
 ];
 return {title:'sync',sub:'Offline capture · sync · conflicts · mobile apps',cards:cards,sections:secs};
};
MOUNT.mobapps=function(el){ const apps=[['Quarry Operations App','Production entry · loading records · equipment inspection · fuel issue · stockpile update · weighbridge lookup · safety report — full offline','pile'],
 ['Construction Site App','Daily site diary · labour attendance · material/equipment requests · progress photos · inspection requests · incident reports — offline sync','site'],
 ['Driver App','Trip assignment · navigation · load details · delivery confirmation & e-POD · fuel entry · breakdown log · GPS breadcrumb','truck'],
 ['Management App','Dashboards · approvals anywhere · exceptions · project & production status · cash position — push alerts','dash']];
 el.innerHTML='<div class="tiles" style="grid-template-columns:repeat(auto-fill,minmax(230px,1fr));padding:10px 12px">'+apps.map(a=>'<div class="tile" style="text-align:left;align-items:flex-start"><div style="display:flex;gap:9px;align-items:center">'+NWX.icon(a[2],20)+'<b>'+a[0]+'</b></div><span style="color:var(--muted);font-weight:400;font-size:11px;line-height:1.5">'+a[1]+'</span><span class="badge green" style="margin-top:4px">PWA · install pushed</span></div>').join('')+'</div>'; };

window.__MH5=1;
})();
