/* =====================================================================
   NOWILLS SQC ERP — Module pack 1
   Executive • Quarry • Drilling & Blasting • Crushing • Weighbridge •
   Stockpile • Sales • CRM • Projects • Contracts • IPC • Site
   ===================================================================== */
(function(){
'use strict';
window.MODULES=window.MODULES||{};
const D=DATA, K=D.K, ts=D.ts, MON=D.MONTHS, C=Charts.palette;
const DAYS30=D.daysLabels(30), HRS=D.hourLabels();
const S=(name,data,o)=>Object.assign({name:name,data:data},o||{});
const ln=(labels,series,ex)=>Object.assign({type:'line',labels:labels,series:series},ex||{});
const ar=(labels,series,ex)=>Object.assign({type:'area',labels:labels,series:series},ex||{});
const br=(labels,series,ex)=>Object.assign({type:'bar',labels:labels,series:series},ex||{});
const hb=(labels,series,ex)=>Object.assign({type:'hbar',labels:labels,series:series},ex||{});
const dn=(items,ex)=>Object.assign({type:'doughnut',items:items},ex||{});
const pi=(items,ex)=>Object.assign({type:'pie',items:items},ex||{});
const gg=(value,ex)=>Object.assign({type:'gauge',value:value,min:ex&&ex.min!=null?ex.min:0,max:ex&&ex.max!=null?ex.max:100},ex||{});
const rg=(value,ex)=>Object.assign({type:'ring',value:value},ex||{});
const wf=(items,ex)=>Object.assign({type:'waterfall',items:items},ex||{});
const hm=(rows,cols,vals,ex)=>Object.assign({type:'heatmap',rows:rows,cols:cols,vals:vals},ex||{});
const scp=(pts,ex)=>Object.assign({type:'scatter',pts:pts},ex||{});
const fnl=(items,ex)=>Object.assign({type:'funnel',items:items},ex||{});
const tm=(items,ex)=>Object.assign({type:'treemap',items:items},ex||{});
const mp=(markers,ex)=>Object.assign({type:'map',markers:markers,height:ex&&ex.height||380,fmt:'num'},ex||{});
const item=(label,value,color)=>({label:label,value:value,color:color});
const fct=(arr,f)=>arr.map(v=>Math.round(v*f));
function f(){return (window.NWX&&NWX.scope)?NWX.scope().f:1;}
window.MH={S:S,ln:ln,ar:ar,br:br,hb:hb,dn:dn,pi:pi,gg:gg,rg:rg,wf:wf,hm:hm,scp:scp,fnl:fnl,tm:tm,mp:mp,item:item,fct:fct,MON:MON,DAYS30:DAYS30,HRS:HRS};

/* scale helpers driven by scope */
function sf(base){return base*f();}

/* ============================ 1. EXECUTIVE ============================ */
MODULES.exec=function(M){
 const L=D.LIVE;
 const cards=[
  K('Total Revenue',48.2e6,{u:'GHS',tgt:52e6,info:'All business lines, group consolidated'}),
  K('Quarry Revenue',22.1e6,{u:'GHS',tgt:24e6}), K('Construction Revenue',18.3e6,{u:'GHS',tgt:19e6}),
  K('Aggregate Sales Revenue',19.4e6,{u:'GHS',tgt:21e6}), K('Equipment-Hire Revenue',2.6e6,{u:'GHS',tgt:2.9e6}),
  K('Haulage Revenue',4.1e6,{u:'GHS',tgt:4.4e6}),
  K('Gross Profit',12.9e6,{u:'GHS',tgt:13.6e6}), K('Operating Profit',7.8e6,{u:'GHS',tgt:8.2e6}),
  K('Net Profit',5.3e6,{u:'GHS',tgt:5.6e6}),
  K('Available Cash',24.8e6,{u:'GHS',neutral:1}), K('Total Bank Balance',31.6e6,{u:'GHS',neutral:1}),
  K('Accounts Receivable',17.4e6,{u:'GHS',good:'down'}), K('Accounts Payable',9.8e6,{u:'GHS',good:'down'}),
  K('Inventory Value',6.9e6,{u:'GHS',neutral:1}), K('Fixed Asset Value',86.2e6,{u:'GHS',neutral:1}),
  K('Active Contract Value',212.9e6,{u:'GHS',neutral:1}), K('Total Project Value',250.4e6,{u:'GHS',neutral:1}),
  K('Project Costs YTD',103.5e6,{u:'GHS',good:'down',tgt:112e6}),
  K('Outstanding Client Certificates',13.1e6,{u:'GHS',good:'down'}),
  K('Outstanding Retentions',11.6e6,{u:'GHS',good:'down'}),
  K('Outstanding Tax Liabilities',2.94e6,{u:'GHS',good:'down'}),
  K('Payroll Cost (MTD)',3.92e6,{u:'GHS',good:'down',tgt:4.1e6}),
  K('Quarry Production Today',L.prodToday,{u:'t',tgt:5200,noscope:0,live:1}),
  K('Quarry Production (Month)',118400,{u:'t',tgt:125000}),
  K('Aggregate Sold Today',L.soldToday,{u:'t',tgt:4600}),
  K('Aggregate Sold (Month)',96400,{u:'t',tgt:105000}),
  K('Cost per Tonne',118.4,{u:'GHS',good:'down',tgt:124,d:1}),
  K('Revenue per Tonne',201.5,{u:'GHS',tgt:198,d:1}), K('Profit per Tonne',54.9,{u:'GHS',tgt:50,d:1}),
  K('Equipment Utilization',74.2,{u:'%',tgt:80,d:1}), K('Fleet Utilization',81.6,{u:'%',tgt:85,d:1}),
  K('Open Safety Incidents',6,{u:'#',good:'down',d:0,tgt:0}),
  K('Environmental Compliance',92.4,{u:'%',tgt:95,d:1})];
 const f1=f();
 const secs=[
  {k:'qa',id:'qa',ti:'Quick Actions'},
  {k:'chart',id:'revt',ti:'Revenue Trend',cls:'wide',spec:ln(DAYS30,[
      S('Quarry',fct(ts(30,760e3,.05,.002,'rq'),f1),C[0],{area:true}),S('Construction',fct(ts(30,610e3,.06,.001,'rc'),f1),C[1],{area:true}),
      S('Equipment Hire',fct(ts(30,88e3,.08,0,'re'),f1),C[3]),S('Haulage',fct(ts(30,138e3,.07,0,'rh'),f1),C[5]),S('Other',fct(ts(30,40e3,.1,0,'ro'),f1),C[6])],
      {fmt:'money',zoom:true,height:300,drillDim:'Revenue · GL 4000-series'})},
  {k:'chart',id:'prof',ti:'Profitability (₵ millions / month)',spec:Object.assign(ln(MON,[
      S('Revenue',fct(ts(12,4.0e6,.06,.008,'pr'),f1),C[0],{type:'bar'}),S('Cost of Sales',fct(ts(12,2.9e6,.05,.006,'pc'),f1),C[4],{type:'bar'}),
      S('Gross Profit',fct(ts(12,1.1e6,.08,.01,'pg'),f1),C[2]),S('Opex',fct(ts(12,0.62e6,.04,.004,'po'),f1),C[6]),S('Net Profit',fct(ts(12,0.44e6,.1,.012,'pn'),f1),C[1],{dash:true})],{fmt:'money'}),{drillDim:'Profitability · P&L'})},
  {k:'chart',id:'cash',ti:'Cash Flow',spec:ar(MON,[S('Inflows',fct(ts(12,4.6e6,.07,0,'ci'),f1),C[2],{area:true}),S('Outflows',fct(ts(12,4.1e6,.05,0,'co'),f1),C[6],{area:true}),S('Net Cash Flow',fct(ts(12,0.5e6,.25,.02,'cn'),f1),C[0])],{fmt:'money',drillDim:'Cash book · bank recs'})},
  {k:'chart',id:'pvs',ti:'Production versus Sales (tonnes)',spec:ln(DAYS30,[S('Produced',fct(ts(30,5050,.05,0,'pp'),f1),C[0],{area:true}),S('Sold',fct(ts(30,4300,.06,.001,'ps'),f1),C[1]),S('Transferred',fct(ts(30,310,.15,0,'pt'),f1),C[3]),S('Internal use',fct(ts(30,180,.2,0,'pi'),f1),C[4]),S('Closing stock index',fct(ts(30,720,.02,0,'pc2'),f1),C[5],{dash:true})],{fmt:'t',zoom:true,height:300,drillDim:'Production ledger · stockpile ledger'})},
  {k:'chart',id:'bud',ti:'Budget versus Actual',spec:br(MON.slice(0,8),[S('Approved Budget',fct(ts(8,4.3e6,.04,0,'bb'),f1),C[8]),S('Actual',fct(ts(8,4.1e6,.05,0,'ba'),f1),C[0]),S('Committed',fct(ts(8,0.9e6,.1,0,'bc'),f1),C[3]),S('Forecast',fct(ts(8,4.35e6,.04,0,'bf'),f1),C[1],{dash:false})],{fmt:'money',drillDim:'Budget GL vs Actual'})},
  {k:'chart',id:'pperf',ti:'Project Performance',spec:hb(D.PROJECTS.map(p=>p.id),[S('Completion %',D.PROJECTS.map(p=>p.comp),C[0]),S('Budget utilisation %',D.PROJECTS.map(p=>Math.round(p.cost/p.bud*100)),C[6]),S('Certified %',D.PROJECTS.map(p=>Math.round(p.cert/p.rc*100)),C[1])],{fmt:'pct',drillDim:'Project ledger'})},
  {k:'chart',id:'revd',ti:'Revenue Distribution',cls:'w4',spec:dn([item('Quarry Operations',Math.round(22.1e6*f1),C[0]),item('Construction',Math.round(18.3e6*f1),C[1]),item('Equipment Hire',Math.round(2.6e6*f1),C[3]),item('Haulage',Math.round(4.1e6*f1),C[5]),item('Material Supply',Math.round(0.9e6*f1),C[4]),item('Other',Math.round(0.2e6*f1),C[8])],{fmt:'money',center:'Revenue',drillDim:'Revenue by segment'})},
  {k:'chart',id:'costd',ti:'Cost Distribution',cls:'w4',spec:dn([item('Materials',Math.round(11.2e6*f1),C[0]),item('Labour',Math.round(6.8e6*f1),C[1]),item('Fuel',Math.round(5.6e6*f1),C[4]),item('Equipment',Math.round(4.9e6*f1),C[3]),item('Maintenance',Math.round(2.2e6*f1),C[5]),item('Subcontractors',Math.round(3.4e6*f1),C[6]),item('Transport',Math.round(1.8e6*f1),C[8]),item('Admin',Math.round(2.4e6*f1),C[9])],{fmt:'money',center:'Cost',drillDim:'Cost by category'})},
  {k:'chart',id:'wc',ti:'Working Capital Trend',cls:'w4',spec:ln(MON,[S('Working Capital',fct(ts(12,15.4e6,.06,.01,'wc'),f1),C[0],{area:true})],{fmt:'money'})},
  {k:'map',id:'gmap',ti:'GIS — Sites, Quarries & Live Deliveries',cls:'wide'},
  {k:'table',id:'alerts',ti:'Alert & Exception Centre — Open Items',cls:'wide',cfg:alertTableCfg(8)},
 ];
 return {title:('dash'),sub:'Group consolidated command centre · live ERP data',cards:cards,sections:secs};
};
function alertTableCfg(n){ return {id:'alerts',page:n||10,freeze:0,search:1,cols:[
  {k:'no',l:'Alert №'},{k:'sev',l:'Severity',y:'badge',badge:v=>sevB(v)},{k:'src',l:'Source'},{k:'mod',l:'Module'},{k:'co',l:'Company'},{k:'site',l:'Quarry / Project'},{k:'desc',l:'Description',wide:1},{k:'own',l:'Owner'},{k:'due',l:'Due',y:'date'},{k:'esc',l:'Escalation'},{k:'st',l:'Status',y:'select',opts:['Open','Acknowledged','In progress','Resolved']}],rows:(window.NWA||[]).slice(0,n||10),
  drill:r=>NWX.alertOpen(r),bulk:[['Acknowledge','ack'],['Resolve','res'],['Escalate','esc']]};}
function sevB(v){return v==='critical'||v==='high'?'red':v==='medium'?'amber':v==='low'?'grey':'blue';}

/* ============================ 2. QUARRY ============================ */
MODULES.quarry=function(M){
 const f1=f(), L=D.LIVE;
 const cards=[K('Total Quarries',3,{u:'#',neutral:1,d:0}),K('Active Quarry Sites',3,{u:'#',neutral:1,d:0}),
  K('Production Today',L.prodToday,{u:'t',tgt:5200}),K('Production This Week',30410,{u:'t',tgt:34500}),
  K('Production This Month',118400,{u:'t',tgt:125000}),K('Production Target (MTD)',125000,{u:'t',neutral:1}),
  K('Target Achievement',94.7,{u:'%',tgt:100,d:1}),
  K('Material Extracted',131200,{u:'t'}),K('Material Crushed',121900,{u:'t'}),K('Saleable Output',116400,{u:'t'}),
  K('Rejected Material',5500,{u:'t',good:'down'}),K('Production Waste',4200,{u:'t',good:'down'}),
  K('Production Yield',95.4,{u:'%',tgt:96,d:1}),K('Recovery Rate',88.7,{u:'%',tgt:90,d:1}),
  K('Cost per Tonne',118.4,{u:'GHS',good:'down',tgt:124,d:1}),K('Revenue per Tonne',201.5,{u:'GHS',tgt:198,d:1}),K('Profit per Tonne',54.9,{u:'GHS',tgt:50,d:1}),
  K('Production Downtime',14.6,{u:'h',good:'down'}),K('Active Shifts',2,{u:'#',neutral:1,d:0}),
  K('Available Equipment',13,{u:'#',neutral:1,d:0}),K('Fuel Used Today',L.fuelToday,{u:'L',good:'down'})];
 const secs=[
  {k:'chart',id:'qdp',ti:'Daily Production — Target vs Actual (t)',cls:'wide',spec:ln(DAYS30,[S('Produced',fct(ts(30,5050,.06,0,'qd'),f1),C[0],{area:true}),S('7-day avg',fct(ts(30,4900,.015,0,'qa'),f1),C[1],{dash:true})],{fmt:'t',zoom:true,target:{value:5200,label:'Target'},live:false,drillDim:'Daily production records',height:300})},
  {k:'chart',id:'qmp',ti:'Monthly Production (t)',spec:br(MON,[S('Produced',fct(ts(12,115e3,.07,.006,'qm'),f1),C[0]),S('Target',fct(ts(12,120e3,.03,0,'qmt'),f1),C[8])],{fmt:'t'})},
  {k:'chart',id:'qbq',ti:'Production by Quarry',spec:dn([item('Shai Hills',Math.round(61500*f1),C[0]),item('Buoho',Math.round(36700*f1),C[1]),item('Nsawam South',Math.round(20200*f1),C[3])],{fmt:'t',center:'MTD'})},
  {k:'chart',id:'qbm',ti:'Production by Material Type',spec:br(['20mm','14mm','10mm','Dust','Rockfill','M-Sand'],[S('MTD',fct([41200,28600,17300,14800,9400,7100],f1),C[0]),S('Last month',fct([39800,27900,16800,15100,9600,7200],f1),C[8])],{fmt:'t'})},
  {k:'chart',id:'qbs',ti:'Production by Shift',spec:pi([item('Day 06–14',43200,C[0]),item('Afternoon 14–22',39800,C[1]),item('Night 22–06',35400,C[3])],{fmt:'t'})},
  {k:'chart',id:'qsup',ti:'Production by Supervisor (t)',spec:hb(['J. Quartey','Y. Frimpong','K. Nkrumah','M. Owusu','S. Teye','A. Darko'],[S('MTD',fct([24100,22800,19400,18600,17300,16200],f1),C[1])],{fmt:'t'})},
  {k:'chart',id:'qcpt',ti:'Cost per Tonne Trend (₵)',spec:ln(DAYS30,[S('₵/t',fct(ts(30,121,.04,0,'qct'),f1),C[4],{area:true})],{fmt:'money',target:{value:124,label:'Cap'},zoom:true})},
  {k:'chart',id:'qyt',ti:'Yield & Recovery Trend (%)',spec:ln(MON,[S('Yield %',ts(12,94.8,.01,.002,'qy'),C[2]),S('Recovery %',ts(12,87.9,.01,.004,'qr'),C[1])],{fmt:'pct',min:80,max:100})},
  {k:'chart',id:'qwr',ti:'Waste % Trend',spec:ar(MON,[S('Waste %',ts(12,3.4,.08,-.01,'qw'),C[6],{area:true})],{fmt:'pct',min:0,max:6})},
  {k:'chart',id:'qrank',ti:'Quarry Performance Ranking (index)',spec:hb(['Shai Hills','Buoho','Nsawam South'],[S('Composite score',fct([96,84,71],1),C[0])],{fmt:'num'})},
  {k:'table',id:'qshift',ti:'Current Production Shifts',cls:'wide',cfg:{id:'qshift',page:6,search:1,cols:[{k:'a',l:'Quarry'},{k:'b',l:'Shift'},{k:'c',l:'Supervisor'},{k:'d',l:'Planned (t)',y:'num'},{k:'e',l:'Actual (t)',y:'num'},{k:'f',l:'Variance',y:'pct'},{k:'g',l:'Equipment'},{k:'h',l:'Operators'},{k:'i',l:'Downtime',y:'num'},{k:'j',l:'Status',y:'badge'}],rows:shiftRows(),totals:['d','e']}},
  {k:'table',id:'qbench',ti:'Active Quarry Benches & Issues',cls:'wide',cfg:{id:'qbench',page:6,cols:[{k:'a',l:'Bench'},{k:'b',l:'Quarry'},{k:'c',l:'Elevation'},{k:'d',l:'Face length'},{k:'e',l:'Geology'},{k:'f',l:'Status',y:'badge'},{k:'g',l:'Open issue'}],rows:benchRows()}}
 ];
 return {title:'quarry',sub:'Extraction • benches • shifts • yield',cards:cards,sections:secs};
};
function shiftRows(){const r=D.mulberry(11);const q=['Shai Hills','Buoho','Nsawam South'];const sup=['J. Quartey','Y. Frimpong','K. Nkrumah','M. Owusu'];
 return q.flatMap(n=>D.SHIFTS.slice(0,2).map((s,i)=>{const pl=2400+r()*400|0,ac=pl*(0.88+r()*0.22)|0;return{a:n,b:s,c:sup[(r()*4)|0],d:pl,e:ac,f:Math.round((ac/pl-1)*1000)/10,g:(3+r()*3|0)+' units',h:(6+r()*5|0),i:Math.round(r()*30)/10+'h',j:ac>=pl?'green':(ac>=pl*0.9?'amber':'red'),jL:ac>=pl?'On target':(ac>=pl*0.9?'Below plan':'Shortfall')}}));}
function benchRows(){return [['L4 North','Shai Hills','74 m','120 m','Granite — fresh','green','Active','—'],['L3 East','Shai Hills','66 m','90 m','Granite — weathered','amber','Watch','Loose crest scaling'],['B2 South','Buoho','58 m','110 m','Granodiorite','green','Active','—'],['B1 West','Buoho','52 m','95 m','Granodiorite','red','Hold','Vibration complaint — EPA'],['N1','Nsawam South','61 m','80 m','Gneiss','green','Active','—'],['N2','Nsawam South','55 m','70 m','Gneiss','blue','Planned','Awaiting blast permit']].map(x=>({a:x[0],b:x[1],c:x[2],d:x[3],e:x[4],f:x[5],fL:x[6],g:x[7]}));}

/* ============================ 3. DRILLING & BLASTING ============================ */
MODULES.drill=function(M){
 const f1=f();
 const cards=[K('Drilling Plans',14,{u:'#',neutral:1,d:0}),K('Active Drilling Jobs',5,{u:'#',neutral:1,d:0}),
  K('Total Holes Drilled',6120,{u:'#'}),K('Total Metres Drilled',89400,{u:'num'}),
  K('Blasts Scheduled',9,{u:'#',neutral:1,d:0}),K('Blasts Completed',41,{u:'#',neutral:1,d:0}),
  K('Explosives Used',18700,{u:'#',info:'kg emulsion YTD'}),K('Detonators Used',6120,{u:'#'}),
  K('Estimated Blast Yield',392000,{u:'t'}),K('Actual Blast Yield',376500,{u:'t'}),
  K('Powder Factor',0.47,{u:'num',d:2,tgt:0.5,good:'down'}),K('Blast Cost',1.86e6,{u:'GHS',good:'down'}),
  K('Blast Cost per Tonne',4.9,{u:'GHS',good:'down',tgt:5.2,d:2}),K('Misfires',1,{u:'#',good:'down',d:0,tgt:0}),
  K('Safety Exceptions',3,{u:'#',good:'down',d:0,tgt:0})];
 const secs=[
  {k:'chart',id:'dm',ti:'Drilled Metres Trend',spec:ln(DAYS30,[S('Metres',fct(ts(30,2980,.09,0,'dm'),f1),C[0],{area:true})],{fmt:'num',zoom:true})},
  {k:'chart',id:'ec',ti:'Explosive Consumption (kg / blast)',spec:br(DAYS30.slice(-14),[S('Emulsion',fct(ts(14,455,.14,0,'ec'),f1),C[4]),S('Boosters',fct(ts(14,42,.1,0,'eb'),f1),C[1])],{fmt:'num'})},
  {k:'chart',id:'eya',ti:'Estimated vs Actual Blast Yield (t)',spec:br(['B-118','B-119','B-120','B-121','B-122','B-123','B-124'],[S('Estimated',fct([9600,11200,8800,10400,12900,9800,11600],f1),C[8]),S('Actual',fct([9200,10800,9100,9700,12400,9600,11200],f1),C[0])],{fmt:'t'})},
  {k:'chart',id:'bc',ti:'Blast Cost Trend',spec:ar(MON,[S('Blast cost',fct(ts(12,152e3,.1,0,'bc'),f1),C[4],{area:true})],{fmt:'money'})},
  {k:'chart',id:'bcpt',ti:'Blast Cost per Tonne by Site (₵)',spec:hb(['Shai Hills','Buoho','Nsawam South'],[S('₵/t',[4.6,5.3,5.8],C[1])],{fmt:'money'})},
  {k:'chart',id:'pf',ti:'Powder-Factor Comparison (kg/t)',spec:hb(['Shai Hills','Buoho','Nsawam South','Design'],[S('PF',[0.44,0.49,0.52,0.5],C[0])],{fmt:'num'})},
  {k:'chart',id:'frag',ti:'Blast Fragmentation Analysis (%)',spec:br(['<100mm','100–300','300–600','600–900','Oversize'],[S('% of muckpile',[8,34,38,14,6],C[2]),S('Kuz-Ram model',[9,36,37,12,6],C[8])],{fmt:'pct'})},
  {k:'chart',id:'bsit',ti:'Blast Safety Incidents (12 months)',spec:ln(MON,[S('Incidents',ts(12,1.1,.5,-.02,'bs'),C[6]),S('Near misses',ts(12,2.4,.4,-.03,'bn'),C[4])],{fmt:'num',min:0})},
  {k:'table',id:'bsched',ti:'Blast Schedule & Clearances',cls:'wide',cfg:{id:'bsched',page:8,cols:[{k:'a',l:'Blast №'},{k:'b',l:'Site / Bench'},{k:'c',l:'Date & time',y:'date'},{k:'d',l:'Holes',y:'num'},{k:'e',l:'Explosives (kg)',y:'num'},{k:'f',l:'Blaster'},{k:'g',l:'Safety clearance',y:'badge'},{k:'h',l:'Explosives stock',y:'badge'},{k:'i',l:'Regulatory approval',y:'badge'},{k:'j',l:'Weather'},{k:'k',l:'Exclusion zone',y:'badge'},{k:'l',l:'Status',y:'badge'}],rows:blastRows()}}
 ];
 return {title:'drill',sub:'Patterns • explosives • fragmentation • clearance workflow',cards:cards,sections:secs};
};
function blastRows(){return [
 {a:'B-125',b:'Shai Hills · L4N',c:'2026-08-12 12:00',d:148,e:4830,f:'S. Teye (Lic #GH-2210)',g:'green',gL:'Cleared',h:'green',hL:'Available',i:'green',iL:'MC approved',j:'Fair · 31°C · SW 8kn',k:'blue',kL:'Set 500 m',l:'blue',lL:'Scheduled'},
 {a:'B-126',b:'Buoho · B2S',c:'2026-08-14 12:30',d:121,e:3950,f:'K. Nkrumah (Lic #GH-1877)',g:'amber',gL:'Pending',h:'green',hL:'Available',i:'amber',iL:'In review',j:'Cloudy · 29°C',k:'grey',kL:'Not set',l:'blue',lL:'Planned'},
 {a:'B-127',b:'Nsawam · N2',c:'2026-08-18 13:00',d:96,e:3120,f:'S. Teye (Lic #GH-2210)',g:'grey',gL:'Not started',h:'amber',hL:'Low stock',i:'red',iL:'Awaiting permit',j:'—',k:'grey',kL:'Not set',l:'amber',lL:'At risk'},
 {a:'B-124',b:'Shai Hills · L3E',c:'2026-08-07 12:00',d:132,e:4410,f:'S. Teye (Lic #GH-2210)',g:'green',gL:'Cleared',h:'green',hL:'Issued',i:'green',iL:'MC approved',j:'Fair · 30°C',k:'green',kL:'Swept & clear',l:'green',lL:'Fired ✓'}];}

/* ============================ 4. CRUSHING ============================ */
MODULES.crush=function(M){
 const f1=f(),L=D.LIVE;
 const cards=[K('Active Crushers',4,{u:'#',neutral:1,d:0}),K('Crushers Running',2,{u:'#',neutral:1,d:0}),
  K('Under Maintenance',1,{u:'#',good:'down',d:0}),K('Crusher Output Today',3020,{u:'t',tgt:3400}),
  K('Crusher Output (Month)',74200,{u:'t',tgt:78000}),K('Plant Utilization',71.8,{u:'%',tgt:78,d:1}),
  K('Machine Hours (MTD)',968,{u:'h'}),K('Downtime Hours',86.4,{u:'h',good:'down'}),
  K('Material Feed Today',3410,{u:'t'}),K('Finished Output Today',3020,{u:'t'}),K('Rejected Output Today',390,{u:'t',good:'down'}),
  K('Crusher Yield',88.6,{u:'%',tgt:90,d:1}),K('Crusher Cost per Tonne',13.7,{u:'GHS',good:'down',tgt:15,d:1}),
  K('Electricity Used',4180,{u:'num',info:'kWh today'}),K('Fuel Consumption',2240,{u:'L',good:'down'})];
 const secs=[
  {k:'html',id:'crushlive',ti:'Live Plant Floor',cls:'wide',live:1},
  {k:'chart',id:'cot',ti:'Crusher Output Trend (t/h)',cls:'wide',spec:ln(HRS.slice(6,22),[S('Crusher A',ts(16,210,.08,0,'ca'),C[0]),S('Crusher B',ts(16,0,0,0,'cb'),C[1],{dash:true}),S('Crusher C',ts(16,165,.09,0,'cc'),C[3])],{fmt:'num',live:true,tickLabel:n=>'+'+n+'h',drillDim:'Crusher telemetry',height:280})},
  {k:'chart',id:'cbo',ti:'Output by Crusher (t MTD)',spec:br(['Crusher A','Crusher B','Crusher C','Crusher D'],[S('MTD',fct([26800,9400,23100,14900],f1),C[0])],{fmt:'t'})},
  {k:'chart',id:'cbs',ti:'Output by Product Size',spec:dn([item('20mm',Math.round(26400*f1),C[0]),item('14mm',Math.round(19800*f1),C[1]),item('10mm',Math.round(14200*f1),C[3]),item('Dust',Math.round(13800*f1),C[8])],{fmt:'t',center:'MTD'})},
  {k:'chart',id:'cut',ti:'Crusher Utilization %',spec:hb(['Crusher A','Crusher B','Crusher C','Crusher D'],[S('Utilisation',[86,34,78,52],C[2]),S('Availability',[92,41,88,81],C[8])],{fmt:'pct'})},
  {k:'chart',id:'cdt',ti:'Downtime by Cause (h MTD)',spec:br(['Mech.','Elec.','Blocked','Wear parts','No feed','Planned'],[S('Hours',[26,12,18,14,9,7],C[6])],{fmt:'h'})},
  {k:'chart',id:'ccpt',ti:'Crusher Cost per Tonne (₵)',spec:ln(MON,[S('₵/t',ts(12,14.2,.04,-.003,'ccpt'),C[4],{area:true})],{fmt:'money'})},
  {k:'chart',id:'cprr',ti:'Product Recovery Rate %',spec:ln(MON,[S('Recovery',ts(12,88,.01,.001,'crr'),C[2])],{fmt:'pct',min:80,max:95})},
  {k:'chart',id:'cfe',ti:'Fuel per Crusher (L/100t)',spec:hb(['Crusher A','Crusher C','Crusher D'],[S('L/100t',[74,81,88],C[4])],{fmt:'L'})},
  {k:'chart',id:'cekwh',ti:'Electricity Consumption (kWh/day)',spec:ar(DAYS30.slice(-14),[S('kWh',fct(ts(14,4100,.08,0,'kw'),f1),C[1],{area:true})],{fmt:'num'})},
  {k:'chart',id:'cpva',ti:'Planned vs Actual Output (t/day)',spec:br(DAYS30.slice(-14),[S('Plan',fct(ts(14,3400,.02,0,'pva'),f1),C[8]),S('Actual',fct(ts(14,3080,.07,0,'pvb'),f1),C[0])],{fmt:'t'})},
  {k:'table',id:'cdown',ti:'Current Downtime & Issues',cls:'wide',cfg:{id:'cdown',page:6,cols:[{k:'a',l:'Crusher'},{k:'b',l:'Started',y:'date'},{k:'c',l:'Cause'},{k:'d',l:'Est. restore'},{k:'e',l:'Work order'},{k:'f',l:'Technician'},{k:'g',l:'Severity',y:'badge'}],rows:[
   {a:'Crusher B — Metso LT120',b:'2026-08-09 06:40',c:'Main bearing overheat (96°C trip)',d:'12–16 h',e:'WO-7882',f:'F. Hammond',g:'red',gL:'Critical'},
   {a:'Crusher D — Kleemann',b:'2026-08-10 05:15',c:'Conveyor CV-4 belt tracking',d:'4 h',e:'WO-7891',f:'T. Abbey',g:'amber',gL:'Medium'},
   {a:'Crusher A — Sandvik',b:'2026-08-08 14:02',c:'Jaw plate wear — output −8%',d:'48 h',e:'WO-7855',f:'F. Hammond',g:'blue',gL:'Planned'}]}}
 ];
 return {title:'crush',sub:'Plant telemetry • yield • downtime',cards:cards,sections:secs};
};

/* ============================ 5. WEIGHBRIDGE ============================ */
MODULES.weigh=function(M){
 const f1=f();
 const cards=[K('Vehicles Weighed Today',L().prodToday?214:214,{u:'#',neutral:1,d:0,tgt:240}),
  K('Gross Weight Today',9860,{u:'t',neutral:1}),K('Tare Weight Today',4210,{u:'t',neutral:1}),K('Net Weight Dispatched',5650,{u:'t',tgt:5400}),
  K('Tonnes Dispatched (MTD)',96400,{u:'t'}),K('Weighbridge Sales Value',1.19e6,{u:'GHS'}),
  K('Cash Transactions',68,{u:'#',d:0}),K('Credit Transactions',146,{u:'#',d:0}),
  K('Unbilled Tickets',12,{u:'#',good:'down',d:0}),K('Cancelled Tickets',3,{u:'#',good:'down',d:0}),
  K('Reprinted Tickets',5,{u:'#',good:'down',d:0}),K('Manual Overrides',2,{u:'#',good:'down',d:0,tgt:0}),
  K('Weight Variances',0.31,{u:'%',good:'down',d:2}),K('Bridge Downtime',0.4,{u:'h',good:'down',d:1})];
 function L(){return D.LIVE;}
 const secs=[
  {k:'html',id:'wqueue',ti:'Live Weighbridge Queue',cls:'wide',live:1},
  {k:'chart',id:'wh',ti:'Tonnes Weighed by Hour',spec:br(HRS.slice(5,20),[S('Net tonnes',fct(ts(15,420,.28,0,'wh'),f1),C[0])],{fmt:'t',live:true,tickLabel:n=>'+'+n+'h'})},
  {k:'chart',id:'wbp',ti:'Tonnes Dispatched by Product',spec:dn(D.PRODUCTS.map((p,i)=>item(p.n,Math.round([21400,25600,18300,12800,9600,8700][i]*f1),C[i%C.length])),{fmt:'t',center:'MTD'})},
  {k:'chart',id:'wbc',ti:'Dispatches by Customer (t)',spec:hb(D.CLIENTS.slice(0,8),[S('Tonnes',fct(ts(8,11800,.25,0,'wcu'),f1),C[1])],{fmt:'t'})},
  {k:'chart',id:'wvc',ti:'Vehicle Count Trend (per day)',spec:ar(DAYS30,[S('Vehicles',fct(ts(30,220,.12,0,'wvc'),f1),C[5],{area:true})],{fmt:'num',zoom:true})},
  {k:'chart',id:'wsv',ti:'Sales Value Trend (₵/day)',spec:ln(DAYS30,[S('₵',fct(ts(30,1.12e6,.1,.001,'wsv'),f1),C[0])],{fmt:'money',zoom:true})},
  {k:'chart',id:'walv',ti:'Average Load per Vehicle (t)',spec:ln(DAYS30,[S('Avg load',ts(30,26.4,.03,0,'wal'),C[2])],{fmt:'t',min:20,max:32})},
  {k:'chart',id:'wop',ti:'Operator Performance (tickets/shift)',spec:hb(['M. Fuseini','E. Sackey','A. Darko','R. Otoo'],[S('Tickets',[96,88,81,76],C[3])],{fmt:'num'})},
  {k:'chart',id:'wct',ti:'Cancelled & Adjusted Tickets (per week)',spec:br(['W28','W29','W30','W31','W32'],[S('Cancelled',[4,2,5,3,3],C[6]),S('Adjusted',[2,3,2,4,2],C[4])],{fmt:'num'})},
  {k:'table',id:'wtickets',ti:'Recent Weighbridge Tickets',cls:'wide',cfg:{id:'wtickets',page:8,search:1,cols:[{k:'a',l:'Ticket №'},{k:'b',l:'Time',y:'date'},{k:'c',l:'Vehicle'},{k:'d',l:'Driver'},{k:'e',l:'Customer'},{k:'f',l:'Product'},{k:'g',l:'Gross (t)',y:'num'},{k:'h',l:'Tare (t)',y:'num'},{k:'i',l:'Net (t)',y:'num'},{k:'j',l:'Value',y:'money'},{k:'k',l:'Payment',y:'badge'},{k:'l',l:'Dispatch',y:'badge'}],rows:ticketRows(),totals:['i','j']}}
 ];
 return {title:'weigh',sub:'Two-bridge operation • GRA e-invoiced tickets',cards:cards,sections:secs};
};
function ticketRows(){const r=D.mulberry(31);const out=[];for(let i=0;i<24;i++){const g=36+r()*10,t=14+r()*3,net=g-t;
 out.push({a:'WB-'+(90210-i),b:'2026-08-10 '+(6+((23-i)/2)|0)+':'+String(10+(i*7)%48).padStart(2,'0'),c:'TRK-'+(1042+((i*149)%560)),d:['A. Lamptey','K. Agyeman','S. Boateng','D. Mensah','P. Ansah'][i%5],e:D.CLIENTS[i%10],f:D.PRODUCTS[i%6].n,g:Math.round(g*10)/10,h:Math.round(t*10)/10,i:Math.round(net*10)/10,j:Math.round(net*D.PRODUCTS[i%6].p),k:i%4?'green':'amber',kL:i%4?'Paid':'Credit',l:i%5?'green':'blue',lL:i%5?'Released':'Queue'});}return out;}

/* ============================ 6. STOCKPILE ============================ */
MODULES.stockpile=function(M){
 const f1=f();
 const cards=[K('Total Stockpile Quantity',102260,{u:'t'}),K('Stockpile Value',10.9e6,{u:'GHS'}),
  K('Raw Material (ROM) Stock',41200,{u:'t'}),K('Finished Aggregate Stock',61060,{u:'t'}),
  K('Produced Today',4820,{u:'t'}),K('Sold Today',3960,{u:'t'}),K('Transferred Today',310,{u:'t'}),K('Used Internally Today',180,{u:'t'}),
  K('Slow-Moving Stock',2400,{u:'t',good:'down'}),K('Non-Moving Stock',860,{u:'t',good:'down'}),
  K('Stock Losses (MTD)',96,{u:'t',good:'down'}),K('Stock Adjustments',14,{u:'#',good:'down',d:0}),
  K('Below Reorder Level',2,{u:'#',good:'down',d:0}),K('Estimated Stock Days',13.4,{u:'num',d:1,info:'days of sales cover'})];
 const secs=[
  {k:'html',id:'pilemap',ti:'Visual Stockpile Map — Shai Hills Yard',cls:'wide'},
  {k:'chart',id:'pbt',ti:'Stockpile Balance Trend (t)',cls:'wide',spec:ar(DAYS30,[S('ROM',fct(ts(30,40800,.03,0,'pb1'),f1),C[8]),S('Finished',fct(ts(30,59600,.04,0,'pb2'),f1),C[0])],{fmt:'t',zoom:true,height:290})},
  {k:'chart',id:'psbp',ti:'Stock by Product (t)',spec:br(D.PRODUCTS.map(p=>p.n),[S('Quantity',fct([18400,11250,4980,22600,3310,4120],f1),C[0])],{fmt:'t'})},
  {k:'chart',id:'psbq',ti:'Stock by Quarry (t)',spec:dn([item('Shai Hills',Math.round(52800*f1),C[0]),item('Buoho',Math.round(31800*f1),C[1]),item('Nsawam South',Math.round(12400*f1),C[3])],{fmt:'t',center:'Stock'})},
  {k:'chart',id:'pad',ti:'Additions vs Deductions (t/day)',spec:br(DAYS30.slice(-14),[S('Produced in',fct(ts(14,5000,.06,0,'ad1'),f1),C[2]),S('Sold / used out',fct(ts(14,4400,.07,0,'ad2'),f1),C[6])],{fmt:'t'})},
  {k:'chart',id:'page',ti:'Inventory Aging (t)',spec:br(['0–15 d','16–30 d','31–60 d','61–90 d','>90 d'],[S('Finished',fct([34800,16200,6400,2800,860],f1),C[1]),S('ROM',fct([28600,9400,3200,0,0],f1),C[8])],{fmt:'t',stacked:true})},
  {k:'chart',id:'psm',ti:'Slow-Moving vs Fast Stock (t)',spec:dn([item('Fast (<15d)',Math.round(52300*f1),C[2]),item('Normal',Math.round(36400*f1),C[0]),item('Slow',Math.round(2400*f1),C[4]),item('Non-moving',Math.round(860*f1),C[6])],{fmt:'t',center:'Flow'})},
  {k:'chart',id:'pvt',ti:'Stock Valuation Trend (₵)',spec:ln(MON,[S('Value',fct(ts(12,10.2e6,.05,.006,'pvt'),f1),C[0],{area:true})],{fmt:'money'})},
  {k:'chart',id:'pps',ti:'Physical vs System Variance (%)',spec:ln(DAYS30.slice(-14),[S('Variance %',ts(14,0.32,.5,0,'pps'),C[4])],{fmt:'pct',min:-1,max:1})},
  {k:'chart',id:'paf',ti:'Availability Forecast (t, 14 days)',spec:ln(DAYS30.slice(-14).concat(['+1d','+2d','+3d','+4d','+5d','+6d','+7d']),
    [S('Actual',fct(ts(14,61000,.02,0,'paf'),f1).concat([null,null,null,null,null,null,null]),C[0]),
     S('Forecast',Array(13).fill(null).concat(fct(ts(8,60600,.015,-.008,'paf2'),f1)),C[1],{dash:true})],{fmt:'t'})},
  {k:'table',id:'pslow',ti:'Reorder & Exception List',cls:'wide',cfg:{id:'pslow',page:8,cols:[{k:'a',l:'Product'},{k:'b',l:'Pile'},{k:'c',l:'On hand (t)',y:'num'},{k:'d',l:'Reorder (t)',y:'num'},{k:'e',l:'Days cover',y:'num'},{k:'f',l:'Last survey',y:'date'},{k:'g',l:'Grade'},{k:'h',l:'Alert',y:'badge'}],rows:[
   {a:'Aggregate 10mm (⅜")',b:'Pile A3 · Shai',c:4980,d:6000,e:4.1,f:'2026-08-04',g:'A',h:'red',hL:'Below reorder'},
   {a:'Quarry Dust',b:'Pile B2 · Buoho',c:22600,d:3000,e:41,f:'2026-08-05',g:'B',h:'amber',hL:'Overstock 141%'},
   {a:'Aggregate 14mm (½")',b:'Pile C1 · Nsawam',c:3310,d:4500,e:3.2,f:'2026-08-01',g:'A',h:'red',hL:'Critical'},
   {a:'Rockfill',b:'ROM East · Buoho',c:4120,d:2000,e:26,f:'2026-08-03',g:'—',h:'green',hL:'OK'}]}}
 ];
 return {title:'stockpile',sub:'Piles • surveys • reconciliations • valuations',cards:cards,sections:secs};
};

/* ============================ 7. SALES ============================ */
MODULES.sales=function(M){
 const f1=f();
 const cards=[K('Sales Today',1.31e6,{u:'GHS',tgt:1.45e6}),K('Sales This Month',19.4e6,{u:'GHS',tgt:21e6}),K('Sales YTD',152.6e6,{u:'GHS'}),
  K('Tonnes Sold (MTD)',96400,{u:'t'}),K('Average Selling Price',201.5,{u:'GHS',d:1,tgt:198}),K('Revenue per Tonne',201.5,{u:'GHS',d:1}),
  K('Gross Margin',27.4,{u:'%',tgt:28,d:1}),K('Cash Sales Today',412e3,{u:'GHS'}),K('Credit Sales Today',898e3,{u:'GHS'}),
  K('New Customers (MTD)',7,{u:'#',d:0}),K('Active Customers',148,{u:'#',d:0,neutral:1}),K('Top Customer','GH Karim & Partners',{u:'text',neutral:1}),
  K('Outstanding Balances',17.4e6,{u:'GHS',good:'down'}),K('Overdue Invoices',34,{u:'#',good:'down',d:0}),
  K('Credit Exposure',9.2e6,{u:'GHS',good:'down'}),K('Sales Orders Pending',18,{u:'#',d:0}),
  K('Deliveries Pending',26,{u:'#',d:0}),K('Quotations Pending',11,{u:'#',d:0})];
 const secs=[
  {k:'chart',id:'dst',ti:'Daily Sales Trend (₵)',cls:'wide',spec:ln(DAYS30,[S('Sales',fct(ts(30,1.24e6,.12,.002,'dst'),f1),C[0],{area:true}),S('7-day avg',fct(ts(30,1.19e6,.03,0,'dst2'),f1),C[1],{dash:true})],{fmt:'money',zoom:true,height:290,drillDim:'Sales ledger · AR'})},
  {k:'chart',id:'mst',ti:'Monthly Sales Trend (₵M)',spec:br(MON,[S('Sales',fct(ts(12,15.6e6,.08,.006,'mst'),f1),C[0]),S('Forecast',fct(ts(12,16.1e6,.03,0,'mstf'),f1),C[8])],{fmt:'money'})},
  {k:'chart',id:'sbp',ti:'Sales by Product',spec:dn(D.PRODUCTS.map((p,i)=>item(p.n,Math.round([5.9e6,6.8e6,4.1e6,1.6e6,0.6e6,0.4e6][i]*f1),C[i%C.length])),{fmt:'money',center:'MTD'})},
  {k:'chart',id:'sbq2',ti:'Sales by Quarry',spec:dn([item('Shai Hills',Math.round(10.4e6*f1),C[0]),item('Buoho',Math.round(6.1e6*f1),C[1]),item('Nsawam South',Math.round(2.9e6*f1),C[3])],{fmt:'money',center:'MTD'})},
  {k:'chart',id:'sbcu',ti:'Sales by Customer (₵ MTD)',spec:hb(D.CLIENTS.slice(0,8),[S('₵',fct(ts(8,2.4e6,.3,0,'sbcu'),f1),C[1])],{fmt:'money'})},
  {k:'chart',id:'sbsp',ti:'Sales by Salesperson',spec:hb(['A. Serwaa','K. Ofori','N. Ashong','D. Bonsu','P. Lamptey'],[S('₵',fct(ts(5,3.8e6,.2,.02,'sbsp'),f1),C[3])],{fmt:'money'})},
  {k:'chart',id:'sbrg',ti:'Sales by Region',spec:pi([item('Greater Accra',Math.round(9.4e6*f1),C[0]),item('Ashanti',Math.round(5.8e6*f1),C[1]),item('Eastern',Math.round(2.6e6*f1),C[3]),item('Western',Math.round(1.1e6*f1),C[4]),item('Volta',Math.round(0.5e6*f1),C[5])],{fmt:'money'})},
  {k:'chart',id:'cvc',ti:'Cash vs Credit Sales',spec:br(MON.slice(0,8),[S('Cash',fct(ts(8,3.9e6,.07,0,'cvc1'),f1),C[2]),S('Credit',fct(ts(8,11.2e6,.05,0,'cvc2'),f1),C[4])],{fmt:'money',stacked:true})},
  {k:'chart',id:'spm',ti:'Sales by Payment Method',spec:pi([item('Bank transfer',Math.round(9.8e6*f1),C[0]),item('Mobile Money',Math.round(3.1e6*f1),C[2]),item('Cheque',Math.round(2.2e6*f1),C[1]),item('Cash',Math.round(1.4e6*f1),C[4]),item('Credit (unpaid)',Math.round(2.9e6*f1),C[6])],{fmt:'money'})},
  {k:'chart',id:'gmt',ti:'Gross Margin Trend (%)',spec:ln(MON,[S('GM %',ts(12,26.8,.04,.002,'gmt'),C[2],{area:true})],{fmt:'pct',min:20,max:32,target:{value:28,label:'Target'}})},
  {k:'chart',id:'agc',ti:'Customer Aging (₵)',spec:br(['Current','1–30d','31–60d','61–90d','>90d'],[S('Outstanding',fct([8.9e6,4.2e6,2.3e6,1.2e6,0.8e6],f1),C[6])],{fmt:'money'})},
  {k:'chart',id:'qcr',ti:'Quotation Funnel (MTD)',spec:fnl([item('Quotations issued',64,C[8]),item('Under negotiation',41,C[1]),item('Accepted',29,C[0]),item('Converted to order',24,C[2])],{fmt:'num'})},
  {k:'chart',id:'sfc',ti:'Sales Forecast (₵, 6 months)',spec:ln(MON.concat(['Sep*','Oct*','Nov*']).slice(0,15),forecastSeries(12,3,1.7e7,'sf'),{fmt:'money'})},
  {k:'table',id:'tops',ti:'Top Customers (YTD)',cls:'wide',cfg:{id:'tops',page:8,search:1,cols:[{k:'a',l:'Customer'},{k:'b',l:'Segment'},{k:'c',l:'Region'},{k:'d',l:'Revenue YTD',y:'money'},{k:'e',l:'Tonnes',y:'num'},{k:'f',l:'Margin %',y:'pct'},{k:'g',l:'Balance',y:'money'},{k:'h',l:'Credit limit',y:'money'},{k:'i',l:'Utilisation',y:'pct'},{k:'j',l:'Status',y:'badge'}],rows:topCustRows(),totals:['d','e','g']}}
 ];
 return {title:'sales',sub:'Orders • dispatches • credit control • forecasts',cards:cards,sections:secs};
};
function forecastSeries(n,ahead,base,seed){const act=ts(n,base,.08,.006,seed);const fc=DATA.forecast(act,ahead);
 return [S('Actual',act.concat(Array(ahead).fill(null)),C[0],{area:true}),S('AI Forecast',Array(n-1).fill(null).concat([act[n-1]]).concat(fc),C[1],{dash:true})];}
function topCustRows(){const r=D.mulberry(41);return D.CLIENTS.slice(0,10).map((c,i)=>{const rev=(4.1e6-i*0.34e6)*(0.9+r()*0.2);const bal=rev*(0.12+r()*0.3);const lim=bal*(1.2+r());
 return {a:c,b:['Contractor','Government','Developer','Logistics'][i%4],c:D.REGIOND[i%6],d:Math.round(rev),e:Math.round(rev/205),f:Math.round((24+r()*9)*10)/10,g:Math.round(bal),h:Math.round(lim),i:Math.round(bal/lim*100),j:bal/lim>0.9?'red':bal/lim>0.7?'amber':'green',jL:bal/lim>0.9?'Near limit':bal/lim>0.7?'Watch':'Healthy'};});}

/* ============================ 8. CRM ============================ */
MODULES.crm=function(M){
 const f1=f();
 const cards=[K('Total Customers',236,{u:'#',d:0,neutral:1}),K('Active Customers',148,{u:'#',d:0,neutral:1}),K('New This Month',7,{u:'#',d:0}),
  K('Dormant (>90d)',21,{u:'#',good:'down',d:0}),K('Avg Days to Pay',32,{u:'num',good:'down',tgt:30}),K('Customer Satisfaction',88,{u:'%',tgt:90}),
  K('Open Support Cases',5,{u:'#',good:'down',d:0}),K('Lifetime Value (top 10)',41.2e6,{u:'GHS',neutral:1})];
 const secs=[
  {k:'chart',id:'ctrend',ti:'Customer Base Growth',spec:ar(MON,[S('Active customers',ts(12,128,.03,.008,'cbg'),C[0],{area:true})],{fmt:'num',min:100})},
  {k:'chart',id:'cpipe',ti:'Sales Pipeline',spec:fnl([item('Leads',36,C[8]),item('Qualified',22,C[1]),item('Quotation',15,C[3]),item('Won',9,C[2])],{fmt:'num'})},
  {k:'chart',id:'cseg',ti:'Revenue by Segment',spec:dn([item('Contractors',Math.round(8.6e6*f1),C[0]),item('Government',Math.round(5.2e6*f1),C[1]),item('Developers',Math.round(3.4e6*f1),C[3]),item('Individuals',Math.round(1.1e6*f1),C[4]),item('Other B2B',Math.round(1.1e6*f1),C[5])],{fmt:'money',center:'Mix'})},
  {k:'table',id:'caccounts',ti:'Customer Accounts & Credit',cls:'wide',cfg:{id:'crm',page:10,search:1,cols:[{k:'a',l:'Customer'},{k:'b',l:'TIN'},{k:'c',l:'Segment'},{k:'d',l:'Sales YTD',y:'money'},{k:'e',l:'Balance',y:'money'},{k:'f',l:'Overdue',y:'money'},{k:'g',l:'Credit limit',y:'money'},{k:'h',l:'Risk',y:'badge'},{k:'i',l:'Last order',y:'date'},{k:'j',l:'Status',y:'badge'}],rows:crmRows()}}
 ];
 return {title:'crm',sub:'Accounts • pipeline • credit risk',cards:cards,sections:secs};
};
function crmRows(){const r=D.mulberry(43);return D.CLIENTS.map((c,i)=>{const bal=r()*2.6e6;const lim=1e6+r()*3e6;const od=r()>.6?bal*(.2+r()*.5):0;
 return {a:c,b:'C000'+(7710000+i*4331),c:['Contractor','Government','Developer','Logistics'][i%4],d:Math.round(1.4e6+r()*3.4e6),e:Math.round(bal),f:Math.round(od),g:Math.round(lim),h:od>lim*0.4?'red':od>0?'amber':'green',hL:od>lim*0.4?'High':od>0?'Watch':'Low',i:'2026-08-0'+(1+(i)%9),j:i%5?'green':'grey',jL:i%5?'Active':'Dormant'};});}

/* ============================ 9. PROJECTS ============================ */
MODULES.projects=function(M){
 const f1=f();const P=D.PROJECTS;
 const act=P.filter(p=>p.st==='active');
 const cards=[K('Total Projects',P.length,{u:'#',d:0,neutral:1}),K('Active Projects',act.length,{u:'#',d:0,neutral:1}),
  K('Completed (FY)',1,{u:'#',d:0}),K('Delayed Projects',2,{u:'#',good:'down',d:0}),K('Suspended',1,{u:'#',good:'down',d:0}),
  K('Total Contract Value',245.9e6,{u:'GHS',neutral:1}),K('Revised Contract Value',259.9e6,{u:'GHS'}),
  K('Project Revenue YTD',103.4e6,{u:'GHS'}),K('Project Cost YTD',96.7e6,{u:'GHS',good:'down'}),K('Gross Profit YTD',6.7e6,{u:'GHS'}),
  K('Average Completion',55.3,{u:'%',d:1}),K('Certified Work',113.6e6,{u:'GHS'}),K('Uncertified Work in Progress',7.8e6,{u:'GHS',neutral:1}),
  K('Outstanding Certificates',13.1e6,{u:'GHS',good:'down'}),K('Retentions Receivable',11.6e6,{u:'GHS',good:'down'}),
  K('Approved Variations',14.0e6,{u:'GHS'}),K('Pending Variations',4.2e6,{u:'GHS',good:'down'}),
  K('Claims Submitted',9,{u:'#',d:0}),K('Claims Approved',5,{u:'#',d:0}),
  K('Projects Over Budget',1,{u:'#',good:'down',d:0}),K('Behind Schedule',3,{u:'#',good:'down',d:0})];
 const secs=[
  {k:'html',id:'gantt',ti:'Portfolio Gantt — drag bars to reschedule',cls:'wide'},
  {k:'chart',id:'pcc',ti:'Completion vs Budget Utilisation',spec:hb(P.map(p=>p.id),[S('Completion %',P.map(p=>p.comp),C[0]),S('Budget used %',P.map(p=>Math.round(p.cost/p.bud*100)),C[6]),S('Certified %',P.map(p=>Math.round(p.cert/p.rc*100)),C[1])],{fmt:'pct'})},
  {k:'chart',id:'prc',ti:'Revenue & Cost by Project (₵M)',spec:hb(P.map(p=>p.id),[S('Revenue',P.map(p=>Math.round(p.cert/1e4)/100),C[2]),S('Cost',P.map(p=>Math.round(p.cost/1e4)/100),C[6])],{fmt:'num'})},
  {k:'chart',id:'ppr',ti:'Profitability Ranking (₵M)',spec:hb(P.map(p=>p.id),[S('GP',P.map(p=>Math.round((p.cert-p.cost)/1e4)/100),C[0])],{fmt:'num'})},
  {k:'chart',id:'pcf',ti:'Project Cash Flow (₵/month)',spec:ar(MON,[S('Certified',fct(ts(12,9.2e6,.1,.01,'pcf1'),f1),C[0],{area:true}),S('Collected',fct(ts(12,7.8e6,.12,.008,'pcf2'),f1),C[2],{area:true}),S('Cost',fct(ts(12,8.1e6,.07,.005,'pcf3'),f1),C[6])],{fmt:'money'})},
  {k:'chart',id:'prisk',ti:'Project Risk Distribution',spec:pi([item('Low',2,C[2]),item('Medium',3,C[4]),item('High',2,C[6])],{fmt:'num'})},
  {k:'chart',id:'pscv',ti:'Cost vs Schedule Variance (₵M / wks)',spec:scp(P.map((p,i)=>({x:Math.round((p.cost-p.bud*p.comp/100)/1e5)/10,y:[-2,3,-1,1,0,2,-4][i],label:p.id,r:8,c:p.risk==='high'?'#ef4444':p.risk==='med'?'#f59e0b':'#22c55e'})),{xl:'Cost variance ₵100k',yl:'Schedule variance (wks)',refX:0,refY:0})},
  {k:'html',id:'pdetail',ti:'Individual Project Command Sheet',cls:'wide'},
  {k:'table',id:'ptable',ti:'Project Portfolio Register',cls:'wide',cfg:{id:'ptable',page:8,search:1,cols:[{k:'a',l:'Project'},{k:'b',l:'Client'},{k:'c',l:'PM'},{k:'d',l:'Contract value',y:'money'},{k:'e',l:'Revised',y:'money'},{k:'f',l:'Completion',y:'pct'},{k:'g',l:'Budget',y:'money'},{k:'h',l:'Actual cost',y:'money'},{k:'i',l:'Certified',y:'money'},{k:'j',l:'Collected',y:'money'},{k:'k',l:'Retention',y:'money'},{k:'l',l:'Finish'},{k:'m',l:'Risk',y:'badge'},{k:'n',l:'Status',y:'badge'}],rows:P.map(p=>({a:p.id+' — '+p.n,b:p.client,c:p.pm,d:p.cv,e:p.rc,f:p.comp,g:p.bud,h:p.cost,i:p.cert,j:p.coll,k:p.ret,l:p.fd,m:p.risk==='high'?'red':p.risk==='med'?'amber':'green',mL:p.risk.toUpperCase(),n:p.st==='done'?'green':p.st==='suspended'?'grey':(p.comp<40&&p.risk==='high')?'red':'blue',nL:p.st==='done'?'Completed':p.st==='suspended'?'Suspended':(p.comp<40&&p.risk==='high')?'At risk':'On track'})),drill:r=>NWX.entity('project',{label:r.a})}}
 ];
 return {title:'projects',sub:'Portfolio • S-curves • variations • claims',cards:cards,sections:secs};
};

/* ============================ 10. CONTRACTS & BOQ ============================ */
MODULES.contracts=function(M){
 const f1=f();const P=D.PROJECTS;
 const cards=[K('Active Contracts',6,{u:'#',d:0,neutral:1}),K('Total Contract Value',245.9e6,{u:'GHS'}),
  K('Contract Variations',14.0e6,{u:'GHS'}),K('Revised Contract Value',259.9e6,{u:'GHS'}),
  K('BOQ Items',1248,{u:'#',d:0,neutral:1}),K('BOQ Completion',51.2,{u:'%',d:1}),
  K('Measured Work Value',113.6e6,{u:'GHS'}),K('Unmeasured Work',146.3e6,{u:'GHS',neutral:1}),
  K('Contract Claims',9,{u:'#',d:0}),K('Extensions of Time',3,{u:'#',d:0,neutral:1}),
  K('Retention Held',11.6e6,{u:'GHS',neutral:1}),K('Performance Bonds Expiring',2,{u:'#',good:'down',d:0}),
  K('Advance Guarantees Expiring',1,{u:'#',good:'down',d:0})];
 const secs=[
  {k:'chart',id:'cvbc',ti:'Contract Value by Client (₵M)',spec:hb(['Min. of Roads','Tema Port Exp.','KMA / AshGold','Kpone LP','Adenta Dev','Volta Infra','GH Karim'],[S('₵M',[92.1,58.4,17.4,23.5,14.8,9.6,44.2],C[1])],{fmt:'num'})},
  {k:'chart',id:'cprog',ti:'Contract Progress (%)',spec:hb(P.map(p=>p.id),[S('Physical %',P.map(p=>p.comp),C[0]),S('Financial %',P.map(p=>Math.round(p.cert/p.rc*100)),C[5])],{fmt:'pct'})},
  {k:'chart',id:'bqva',ti:'BOQ Quantity vs Actual (top items, m³)',spec:br(['Excav.','Fill','Subbase','Base','Asphalt','Conc.','Drain'],[S('BOQ qty',fct([180,142,96,88,54,72,61],1e3),C[8]),S('Actual',fct([98,74,52,41,26,39,33],1e3),C[0])],{fmt:'num'})},
  {k:'chart',id:'bqvc',ti:'BOQ Value vs Certified (₵M)',spec:br(['Earthworks','Pavement','Structures','Drainage','Prelims','Dayworks'],[S('BOQ',fct([61,88,42,24,18,12],1e6),C[8]),S('Certified',fct([33,39,21,9,9,2.6],1e6),C[2])],{fmt:'money'})},
  {k:'chart',id:'vot',ti:'Variation-Order Trend (₵/quarter)',spec:br(['Q1','Q2','Q3','Q4','Q1-26','Q2-26','Q3-26'],[S('VOs',fct([1.2,2.8,1.6,3.9,2.2,4.4,2.1],1e6),C[3])],{fmt:'money'})},
  {k:'chart',id:'csd',ti:'Claim Status Distribution',spec:dn([item('Approved',5,C[2]),item('Under review',3,C[4]),item('Rejected',1,C[6])],{fmt:'num',center:'Claims'})},
  {k:'chart',id:'rt',ti:'Retention Accumulation (₵M)',spec:ln(MON,[S('Retention',fct(ts(12,9.4e6,.04,.015,'rt'),f1),C[4],{area:true})],{fmt:'money'})},
  {k:'chart',id:'cprof',ti:'Contract Profitability (%)',spec:hb(P.map(p=>p.id),[S('Margin %',P.map(p=>Math.round((p.cert-p.cost)/p.cert*1000)/10),C[2])],{fmt:'pct'})},
  {k:'chart',id:'cexp',ti:'Contract Expiry Timeline',spec:{type:'gantt',ganttSimple:1,fmt:'num',legend:false,rows:P.map(p=>({name:p.id,ps:p.sd,pe:p.pd,as:p.sd,ae:p.fd,prog:p.comp,status:p.st==='done'?'done':(p.risk==='high'?'delayed':'ok')}))}},
  {k:'table',id:'ctable',ti:'Contracts Register',cls:'wide',cfg:{id:'ctable',page:8,cols:[{k:'a',l:'Contract'},{k:'b',l:'Client'},{k:'c',l:'Original value',y:'money'},{k:'d',l:'Variations',y:'money'},{k:'e',l:'Revised',y:'money'},{k:'f',l:'Retention (5%)',y:'money'},{k:'g',l:'Bond expiry'},{k:'h',l:'EOT status',y:'badge'},{k:'i',l:'Status',y:'badge'}],rows:P.map(p=>({a:p.id+' — '+p.n,b:p.client,c:p.cv,d:p.rc-p.cv,e:p.rc,f:p.ret,g:p.st==='done'?'2026-08-30':'2027 Q1',h:p.var?'amber':'grey',hL:p.var?'EOT claimed':'—',i:p.st==='done'?'green':p.st==='suspended'?'grey':'blue',iL:p.st==='done'?'Closed out':p.st==='suspended'?'Suspended':'Live'}))}}
 ];
 return {title:'contracts',sub:'BOQ measurement • VOs • claims • securities',cards:cards,sections:secs};
};

/* ============================ 11. IPC ============================ */
MODULES.ipc=function(M){
 const f1=f();
 const cards=[K('Applications Submitted',86,{u:'#',d:0}),K('Certificates Approved',72,{u:'#',d:0}),K('Pending',9,{u:'#',d:0,neutral:1}),
  K('Rejected',5,{u:'#',good:'down',d:0}),K('Gross Certified Value',113.6e6,{u:'GHS'}),K('Retention Deducted',11.6e6,{u:'GHS'}),
  K('Advance Recovered',8.2e6,{u:'GHS'}),K('Tax Deducted (WHT)',5.9e6,{u:'GHS'}),K('Net Certified',88.1e6,{u:'GHS'}),
  K('Amount Collected',75.0e6,{u:'GHS'}),K('Outstanding Certified',13.1e6,{u:'GHS',good:'down'}),
  K('Avg Certification Time',14,{u:'num',good:'down',tgt:14,info:'days'}),K('Avg Payment Time',29,{u:'num',good:'down',tgt:30,info:'days'})];
 const secs=[
  {k:'chart',id:'cibm',ti:'Certificates Issued by Month (₵M)',spec:br(MON,[S('Certified',fct(ts(12,9.4e6,.12,.01,'cibm'),f1),C[0])],{fmt:'money'})},
  {k:'chart',id:'cvcert',ti:'Certified vs Collected (cumulative ₵M)',spec:ln(MON,[S('Certified',fct(ts(12,64e6,.03,.03,'cvc1'),f1),C[0]),S('Collected',fct(ts(12,55e6,.03,.03,'cvc2'),f1),C[2])],{fmt:'money'})},
  {k:'chart',id:'oca',ti:'Outstanding Certificate Aging (₵M)',spec:br(['Current','1–30d','31–60d','61–90d','>90d'],[S('Outstanding',fct([5.4,3.8,2.1,1.1,0.7],1e6),C[6])],{fmt:'money'})},
  {k:'chart',id:'ra',ti:'Retention Accumulation (₵M)',spec:ar(MON,[S('Retention',fct(ts(12,9.4e6,.04,.015,'ra'),f1),C[4],{area:true})],{fmt:'money'})},
  {k:'chart',id:'art',ti:'Advance Recovery Trend (₵)',spec:br(MON.slice(0,8),[S('Recovered',fct(ts(8,1.02e6,.09,0,'art'),f1),C[3])],{fmt:'money'})},
  {k:'chart',id:'cvbp',ti:'Certificate Value by Project (₵M)',spec:dn(D.PROJECTS.map((p,i)=>item(p.id,Math.round(p.cert/1e5)/10,C[i%C.length])),{fmt:'num',center:'₵M'})},
  {k:'chart',id:'cpp',ti:'Client Payment Performance (avg days)',spec:hb(['Min. of Roads','Tema Port Exp.','KMA / AshGold','Kpone LP','Adenta Dev','Volta Infra'],[S('Avg days',[38,26,44,21,19,31],C[4])],{fmt:'num'})},
  {k:'table',id:'ipct',ti:'Certificates Register',cls:'wide',cfg:{id:'ipct',page:9,search:1,cols:[{k:'a',l:'IPC №'},{k:'b',l:'Project'},{k:'c',l:'Submitted',y:'date'},{k:'d',l:'Gross',y:'money'},{k:'e',l:'Retention',y:'money'},{k:'f',l:'Advance rec.',y:'money'},{k:'g',l:'WHT',y:'money'},{k:'h',l:'Net certified',y:'money'},{k:'i',l:'Paid',y:'money'},{k:'j',l:'Days o/s',y:'num'},{k:'k',l:'Status',y:'badge'}],rows:ipcRows(),totals:['d','e','h','i']}}
 ];
 return {title:'ipc',sub:'Measurement → certification → cash',cards:cards,sections:secs};
};
function ipcRows(){const r=D.mulberry(51);const out=[];const sts=[['green','Paid'],['amber','Certified — awaiting payment'],['blue','Submitted'],['grey','Rejected — re-measure']];
 for(let i=0;i<14;i++){const p=D.PROJECTS[i%7];const g=Math.round((2.1+r()*4.4)*1e6);const ret=Math.round(g*0.05),adv=Math.round(g*0.06),wht=Math.round(g*0.075);const net=g-ret-adv-wht;const paid=sts[i%4][0]==='green'?net:(sts[i%4][0]==='amber'?Math.round(net*0.3):0);
  out.push({a:'IPC-2026-'+(141-i),b:p.id,c:'2026-0'+(4+(i%4))+'-'+(10+(i%17)),d:g,e:ret,f:adv,g:wht,h:net,i:paid,j:i%3?'':18+i*7,k:sts[i%4][0],kL:sts[i%4][1]});}
 return out;}

/* ============================ 12. SITE ============================ */
MODULES.site=function(M){
 const f1=f();
 const cards=[K('Active Sites',6,{u:'#',d:0,neutral:1}),K('Site Reports Today',6,{u:'#',d:0,tgt:6}),
  K('Workforce on Site',412,{u:'#'}),K('Plant on Site',38,{u:'#',d:0,neutral:1}),
  K('Concrete Poured (MTD)',2140,{u:'num',info:'m³'}),K('Asphalt Laid (MTD)',8600,{u:'t'}),
  K('Open NCRs',4,{u:'#',good:'down',d:0}),K('Open Snags',31,{u:'#',good:'down',d:0}),
  K('Lost Productive Days (rain)',2.5,{u:'num',d:1,good:'down'}),K('Site Safety Walks (wk)',12,{u:'#',d:0})];
 const secs=[
  {k:'chart',id:'sman',ti:'Workforce by Site (today)',spec:hb(['Tema Interchange','Kpone LP','Volta Bridge','Buoho Twp','Nsawam–Pokuase','Shai Crusher'],[S('Workers',[124,86,41,63,52,46],C[0])],{fmt:'num'})},
  {k:'chart',id:'sprog',ti:'Physical Progress — daily reports (%)',spec:ln(DAYS30,[S('Planned',ts(30,1.6,.1,.01,'sp1'),C[8],{dash:true}),S('Actual',ts(30,1.52,.12,.01,'sp2'),C[0],{area:true})],{fmt:'pct',zoom:true})},
  {k:'chart',id:'srain',ti:'Rain Downtime (h/day)',spec:br(DAYS30.slice(-14),[S('Hours',[0,0,1.5,0,3.2,0,0,2.1,0,0,1.1,4.0,0,0],C[1])],{fmt:'h'})},
  {k:'table',id:'sdiary',ti:'Site Diaries',cls:'wide',cfg:{id:'sdiary',page:8,cols:[{k:'a',l:'Report №'},{k:'b',l:'Site'},{k:'c',l:'Date',y:'date'},{k:'d',l:'Foreman'},{k:'e',l:'Gang',y:'num'},{k:'f',l:'Plant'},{k:'g',l:'Activity'},{k:'h',l:'Delays'},{k:'i',l:'Visitor'},{k:'j',l:'Status',y:'badge'}],rows:diaryRows()}}
 ];
 return {title:'site',sub:'Diaries • gangs • NCRs • weather',cards:cards,sections:secs};
};
function diaryRows(){const r=D.mulberry(61);const sites=['Tema Interchange','Kpone LP','Volta Bridge','Buoho Twp Roads','Nsawam–Pokuase'];
 return sites.flatMap((s,si)=>[0,1,2].map(d=>({a:'DSR-'+(3310-si*3-d),b:s,c:'2026-08-'+(10-d),d:['K. Boateng','S. Adjei','D. Amoah','A. Mensah','E. Osei'][si],e:28+((r()*40)|0),f:(3+r()*5|0)+' units',g:['Base course placing','Box culvert B12','Slope stabilisation','Surfacing prep','Girder launch'][si],h:r()>.7?'Rain 2h':'—',i:r()>.6?'Client RE':'—',j:'green',jL:d?'Submitted':'Approved'})));}

window.__MH1=1;
})();
