ACP ERP  ·  Infrastructure
Inspire Africa Coffee Park  ·  Ntungamo, Uganda
?
← Dashboard
Portal ERP | HR Finance Production Quality Stores Procurement Infra Sales Office Agri Tourism Export RDP POW Analytics Inventory Mail
🏗️
DepartmentInfrastructure
👤
Module ManagerNedy Edrine Muziru
🏢
LocationRwashameire, Ntungamo
SystemChecking...
📅
Date
Time
📆
Fiscal YearFY 2025/2026
Construction Zones — Progress Grid
Zone Progress — All 16 Zones
Contractor Payment Status
Budget Allocated vs Spent (Top 8 Zones)
Recent Site Log Entries
Portfolio Summary — April 2026
Zone Progress
Master Tracker — All Contracts
Data source: ACP_Master_FINAL_Merged2.xlsx — March / April 2026
Financial Dashboard
Data source: ACP_Master_FINAL_Merged2.xlsx — March / April 2026
Physical Progress Dashboard
Data source: ACP_Master_FINAL_Merged2.xlsx — March / April 2026
Construction Progress — Contractor View
Data source: ACP_Master_FINAL_Merged2.xlsx — March / April 2026
Payment Voucher — April 2026
CEO Approval column is editable. Amounts in UGX millions for display; full figures stored.
Payment History — All Rounds
Data source: ACP_Master_FINAL_Merged2.xlsx — March / April 2026
Cement Store — March 2026
Steel Store — March 2026
General Store Register
Engineers & Site Staff Register
Zone Reference
Data source: ACP_Master_FINAL_Merged2.xlsx — April 2026
REF_CONTRACTORS — Financial Registry
Editable registry — changes saved to localStorage. Blue = editable, Green = formula.
Contractor Forms — Sub-Contract Cards
Progress Input — Supervisor Update Sheet
Auto-saves on change (800ms debounce). Updates % progress and status live across all tabs.
Contract Records — Individual Contractor File
Structures Register — 81 Structures
Click any row to see contracts linked to that structure. Supervisor column is editable.
Lists Reference — REF_LISTS Dropdown Validation Tables
Read-only reference — these values drive all dropdowns across the Master Tracker module.
Construction Zones (16)
#Zone NameContractorProgressStatusBudget (UGX)Spent (UGX)BalanceStart DateTarget DateAttachmentsActions
Zone Detail
Zones — Reports & Statistics

Loading statistics...

Zone Progress Inspection
Contractors & Vendors
NameContactSpecialityContract ValuePaidBalanceStatusRatingActive ZonesLast PaymentActions
Payment Schedule & Milestones
Contractors — Reports & Statistics

Loading statistics...

Contractor Performance

Performance Ranking

ContractorZones AssignedAvg Zone ProgressSnags OpenRatingPayment ClaimsScore
INSPIRE AFRICA COFFEE PARK — MONTHLY PROGRESS REPORT ON CONSTRUCTION AND CONTRACTORS PROGRESS
Reporting Month: March 2026 | Prepared by: Tumuramye Julius / Kyorishaba Favour | Revised by: Allan Nkabahita
Active Contractors
19
Main Site
Ongoing Works
14
Active progress
Halted Works
3
Stopped this month
Inactive
2
Not on site
#ContractorSection of WorkProgress %StatusKey AchievementWorkersRemarksActions
INSPIRE AFRICA GROUP — MONTHLY PROGRESS REPORT ON CONSTRUCTION WORKS — NYABIHOKO
Reporting Month: March 2026 | Prepared by: Christine Blessing
#ContractorSection of WorkProgress %StatusKey AchievementRemarksActions
MATERIAL REQUESTS — NYABIHOKO SITE:
Hard Core — HIGH URGENCY  Cement CEM IV — MEDIUM  Cement CEM II — MEDIUM  Sand and Aggregates — MEDIUM  Stone Dust — MEDIUM
CONCLUSION: Labourers need more financial support to motivate them to be more active for better progress of work. Immediate attention should be given to urgent materials on site.
Key Challenges
Site Staff Daily Record
Toolbox Meeting Observations
Progress Report — Statistics

Loading...

SITE CONTRACTORS PAYMENT PLAN — As at 08/04/2026 | Total Contractors: 41
Total Contract Value
UGX 6.92B
6,919,126,892
Total Paid to Date
UGX 3.92B
3,916,101,500
Total Outstanding
UGX 3.0B
3,003,025,392
Proposed (April)
UGX 756.8M
756,840,000
Payment Rate
56.6%
Paid vs Contract
Active Contractors
41
All contracts
#Contractor NameContract Sum (UGX)Payments Made (UGX)Balance (UGX)Requested (UGX)Proposed (UGX)Payment %Actions
TOTALS 6,919,126,8923,916,101,5002,914,469,1501,467,680,000756,840,00056.6%
Payment Summary Analysis

Top 5 Contractors by Contract Value

Payment Status Distribution

ContractorContract SumPaidProposed% CompleteStatus
Payments — Reports & Statistics

Loading...

Payment Certificate Generator
Site Measurement Sheets
RefContractorBuilding / ZonePeriod Gross This PeriodNet This PeriodStatusActions
Interim Payment Certificate Generator

Financial Statement

Approval Chain

Payment Runs
PR NoDateAuthorised ByMethod No. of IPCsTotal Amount (UGX)StatusActions
IPC Register — Full Ledger
IPC NoDateContractorBuilding / Zone Gross ValueNet CertifiedRetention Held StatusLinked MSActions
Retention Ledger
ContractorTotal Retention Held% Complete Release ConditionReleasedBalance HeldActions
Equipment Register
Asset IDNameCategoryZone/LocationStatusConditionPurchase DatePurchase ValueLast ServiceNext Service DueAttachmentsActions
Equipment Service Due
Equipment — Reports & Statistics

Loading...

Equipment Zone Assignment
Asset IDNameCategoryCurrent ZoneStatusActions
Daily Site Log
DateZoneContractorSubmitted ByWeatherLaborProgress %IssuesAttachmentsActions
Weekly Summary
Site Log — Reports & Statistics

Loading...

Photo Gallery
Open Snag Items
Snag #ZoneDescriptionCategorySeverityRaised ByDate RaisedAssigned ToTarget CloseStatusAttachmentsActions
Completed / Closed Snags
Snag #ZoneDescriptionCategorySeverityClosed DateInspector
Snags by Zone
Snag List — Reports & Statistics

Loading...

Total BoQ (excl. contingency+VAT)
UGX 161.7B
161,705,607,881
Total Project Cost (incl. all)
UGX 200.4B
200,353,248,164
Coffee Park Bill
UGX 121.9B
121,885,287,637
Nyabihoko Bill
UGX 25.8B
25,762,367,144
Warehouse Equipment
UGX 14.1B
14,057,953,100
Contingency (5%)
UGX 8.1B
8,085,280,394
VAT @ 18%
UGX 30.6B
30,562,359,889
QS Firm
Frank Odoi & Assoc.
IAG/COFFEE PARK BOQS
Main Summary — All Bills
Bill NoDescriptionSub-Total (UGX)Contingency 5%Sub-Total + Cont.VAT 18%Grand Total (UGX)
1Coffee Park121,885,287,6376,094,264,382127,979,552,01923,036,319,363151,015,871,382
2Nyabihoko (Coffee Tourism)25,762,367,1441,288,118,35727,050,485,5014,869,087,39031,919,572,891
3Warehouse Equipment14,057,953,100703,897,65514,761,850,7552,657,133,13617,418,983,891
SUB TOTAL161,705,607,8818,085,280,394169,790,888,27530,562,359,889200,353,248,164
TOTAL ESTIMATED PROJECT COST: UGX 200,353,248,164
Reference: IAG/COFFEE PARK BOQS — Quantity Surveying by Frank Odoi & Associates
Prepared for: Inspire Africa Group | Location: Rwashameire, Ntungamo District, Uganda
Coffee Park — Bill 1 Breakdown (23 Buildings)
#Building / ZoneBoQ Sub-Total (UGX)Contingency 5%Total incl. VAT (UGX)% of BillProgressEdit
Nyabihoko (Coffee Tourism) — Bill 2 Breakdown
#Building / ZoneBoQ Sub-Total (UGX)Contingency 5%VAT 18%Grand Total (UGX)% of BillEdit
Electrical Installations — Full Sub-Breakdown (Bill Total: UGX 13,798,876,000)
#Area / BuildingAmount (UGX)% of Electrical Bill
Individual Building BoQ Detail
Quantity Surveying Work Progress — Status Tracker
INSPIRE AFRICA COFFEE PARK — QUANTITY SURVEYING WORK PROGRESS
QS Firm: Frank Odoi & Associates | Reference: IAG/COFFEE PARK BOQS
S/NDescriptionSiteBoQ Sub-Total (UGX)BOQ StatusLast Updated
BOQ — Reports & Statistics

Loading...

Variation Orders
VO #DateContractorDescriptionReasonVO Value (UGX)Cumulative VOsApprovedApproved ByDate ApprovedActions
Documents Register
Doc #TitleTypeZoneContractorRevisionDate IssuedStatusUploaded ByActions
Documents by Zone
Documents by Contractor
Pre-built Reports
#Report NameDescriptionAction
1Construction Progress ReportAll 16 zones with progress %, status, contractor, and budget
2Contractor Performance ReportContractor ratings, contract values, and zone performance
3Equipment Status ReportAll equipment with condition, status, and service schedule
4Site Log SummaryAll site log entries with zone, contractor, and progress notes
5Snag List Report (Open Items)All open snag items by zone and severity
6Variation Orders SummaryAll VOs with values, approval status, and cumulative impact
7Budget vs Actual by ZoneBudget allocated vs amount spent per zone with variance
8Equipment Service ScheduleAll equipment service dates, next due, and overdue items
Live Data Charts
Generate Custom Report
📋
ModuleSupervision Consultants
👤
OversightNedy Edrine Muziru
📍
SiteRwashameire, Ntungamo
KubarihoActive
Consultant Register
#NameFirm / CompanySpecialisation License NoPhoneEmail Contract StartContract EndRate (UGX)StatusActions
Zone Assignments
#Consultant NameAssigned Zone(s)Project Phase Start DateEnd DateDays on SiteStatusActions
Site Visit Log
#Visit DateConsultantZonePurpose Findings SummaryPhotosNext VisitActions
Reports Received from Consultants
#Report TitleConsultantZoneType Date SubmittedDue DateStatusReviewActions
Consultant Performance Scorecard
#NameFirmZonesSite Visits ReportsOn Time %Avg QualityNCRs RaisedOverall Rating
Consultant Fee Tracker
#ConsultantPeriodFee TypeAmount (UGX) Invoice NoInvoice DateDue DatePayment DateStatusActions
Supervision — Reports & Statistics

Loading...

Non-Conformance Reports (NCRs)

How to Use — Infrastructure & Construction

A guide to using this module effectively. Click any section to expand.

🏗Construction Zones
  1. Go to Zones to see all 16 construction zones on the 150-acre estate.
  2. Click on a zone to view its contractor, progress percentage, budget, and site log.
  3. Click + Log Zone Update to record today's site progress — enter completion %, work done, and issues.
  4. The Overview tab auto-calculates weighted progress across all zones.
Tip: Update zone progress every site visit day — the CEO dashboard pulls this data in real time.
👷Contractors
  1. Go to Contractors to view all registered contractors, their contract values, amounts paid, and performance ratings.
  2. Click + Add Contractor to register a new contractor with their contact details, speciality, and contract value.
  3. Record payments against each contractor as work is certified.
  4. Rate contractor performance on completion of each milestone.
Tip: All contractor payments above UGX 10M require sign-off by Kubariho Venture and Nelson Tugume.
📋Work Orders & Snag List
  1. Go to Snag List to view all defects and issues identified during inspection.
  2. Click + Add Snag — fill in zone, description, priority (High/Medium/Low), and responsible contractor.
  3. Once a defect is rectified, change its status to Closed.
  4. High-priority snags must be escalated to Kubariho Venture immediately.
Tip: Never mark a snag as Closed without a site inspection confirmation photo or sign-off.
📄Documents & BOQ
  1. Go to BOQ & Variations to track Bill of Quantities and approved scope changes.
  2. Record each variation order with its justification, value change, and approval reference.
  3. Go to Documents to store drawings, specifications, and permits — upload URL or attach description.
Tip: All variation orders must be approved in writing by the CEO before implementation begins.
');w.document.close();w.print(); } // ── 7. NCRs ──────────────────────────────────────────────── function renderSvNCRs(){ const ncrs=getSvData('erp_supervision_ncrs',SV_NCRS_SEED); const kpis=document.getElementById('sv-ncrs-kpis'); if(kpis){ const open=ncrs.filter(n=>n.status==='Open').length; const closed=ncrs.filter(n=>n.status==='Closed').length; const critical=ncrs.filter(n=>n.severity==='Critical').length; const today=new Date().toISOString().slice(0,10); const thisMonth=ncrs.filter(n=>n.date&&n.date.startsWith('2026-04')).length; kpis.innerHTML=`
Open NCRs
${open}
Closed
${closed}
Critical
${critical}
Raised This Month
${thisMonth}
`; } const list=document.getElementById('sv-ncrs-list'); if(!list)return; const sorted=[...ncrs].sort((a,b)=>{ const sev={Critical:0,Major:1,Minor:2}; if(a.status==='Open'&&b.status!=='Open')return -1; if(b.status==='Open'&&a.status!=='Open')return 1; return (sev[a.severity]||3)-(sev[b.severity]||3); }); list.innerHTML=sorted.map(n=>`
${n.id} • Raised ${n.date} • Response Due ${n.due}
${n.desc.slice(0,120)}${n.desc.length>120?'...':''}
Raised by: ${n.raisedBy} • Against: ${n.against}
Zone: ${n.zone}
${svSevBadge(n.severity)} ${svBadgeStatus(n.status)}
${n.resolution?`
Resolution: ${n.resolution}
`:''}
${n.status==='Open'?``:''}
`).join(''); } function saveSvNCR(){ const cEl=document.getElementById('sv-ncr-by');const raisedBy=cEl?cEl.value:''; const desc=document.getElementById('sv-ncr-desc').value.trim(); if(!raisedBy||!desc)return alert('Raised-by consultant and description are required.'); const ncrs=getSvData('erp_supervision_ncrs',SV_NCRS_SEED); const id='NCR-'+(String(ncrs.length+1).padStart(3,'0')); ncrs.unshift({ id,raisedBy, against:document.getElementById('sv-ncr-against').value, zone:document.getElementById('sv-ncr-zone').value, desc, severity:document.getElementById('sv-ncr-severity').value, date:document.getElementById('sv-ncr-date').value, due:document.getElementById('sv-ncr-due').value, status:'Open', resolution:'' }); saveSvData('erp_supervision_ncrs',ncrs); addActivity('NCR raised: '+id+' by '+raisedBy); closeModal('sv-ncr-modal');renderSvNCRs(); } function closeSvNCR(id){ const resolution=prompt('Enter resolution notes to close NCR '+id+':'); if(resolution===null)return; let ncrs=getSvData('erp_supervision_ncrs',SV_NCRS_SEED); const i=ncrs.findIndex(n=>n.id===id); if(i<0)return; ncrs[i].status='Closed';ncrs[i].resolution=resolution||'Closed without notes.'; saveSvData('erp_supervision_ncrs',ncrs);renderSvNCRs(); } function delSvNCR(id){ if(!confirm('Remove this NCR record?'))return; let n=getSvData('erp_supervision_ncrs',SV_NCRS_SEED); n=n.filter(x=>x.id!==id);saveSvData('erp_supervision_ncrs',n);renderSvNCRs(); } // ── Supervision module init (called when tab is activated) ─ function initSupervisionModule(){ svPopulateSelects(); renderSvRegister(); } // ============================================================ // REAL CONTRACTOR LIST RENDER // ============================================================ function renderRealContractorList(){ // Delegate to unified master-data renderer renderRealContractorListNew(); } // ============================================================ // BOQ REAL DATA RENDER FUNCTIONS // ============================================================ const BOQ_COFFEE_PARK=[ {n:1,name:'Admin, Roastry & Cosmetics',sub:9812393701,cont:490619685,grand:12157555796}, {n:2,name:'Staff Accommodation Blocks',sub:3551772684,cont:177588634,grand:4400646355}, {n:3,name:'Convention Centre',sub:5331729133,cont:266586457,grand:6606012396}, {n:4,name:'Boiler 1 Building',sub:957177629,cont:47858881,grand:1185943082}, {n:5,name:'Boiler 2 Building',sub:1011607563,cont:50580378,grand:1253381771}, {n:6,name:'Woodshade Warehouse Building 01',sub:1085780299,cont:54289015,grand:1345281790}, {n:7,name:'Wood Shade Warehouse Bldg 2 (w/ Basement)',sub:2185873645,cont:109293682,grand:2708297446}, {n:8,name:'Frezy Dry & Cold Brew Building',sub:4478428398,cont:223921420,grand:5548772785}, {n:9,name:'Spray Dryer Building',sub:5182370279,cont:259118514,grand:6420956775}, {n:10,name:'Spray Dryer Building Extension',sub:5136870279,cont:256843514,grand:6364582275}, {n:11,name:'Evaporator Building',sub:1204856795,cont:60242840,grand:1492817569}, {n:12,name:'Wet & Dry Mill Building',sub:7515383379,cont:375769169,grand:9311560006}, {n:13,name:'Coffee Academy',sub:14378424329,cont:718921216,grand:17814867744}, {n:14,name:'Agricultural Offices',sub:385896524,cont:19294826,grand:478125794}, {n:15,name:'Batch Plant',sub:293735526,cont:14686776,grand:363938317}, {n:16,name:'Cement Storage Structure',sub:1119146149,cont:55957307,grand:1386622078}, {n:17,name:'Conference Hall',sub:390761024,cont:19538051,grand:484152909}, {n:18,name:'Generator House',sub:542302304,cont:27115115,grand:671912555}, {n:19,name:'Electrical Installations',sub:13798876000,cont:0,grand:0,note:'See Electrical BoQ tab'}, {n:20,name:'Extractor',sub:1342157895,cont:67107895,grand:1662933632}, {n:21,name:'Store 01 & 02',sub:2793862363,cont:139693118,grand:3461595468}, {n:22,name:'Raw Materials & Finished Goods',sub:5434556570,cont:0,grand:0,note:'Contingency/VAT TBC'}, {n:23,name:'External Works',sub:33951325169,cont:0,grand:0,note:'Contingency/VAT TBC'} ]; const BOQ_NYABIHOKO=[ {n:1,name:'Cattle Shade',amount:2356188584}, {n:2,name:'Chicken Shade',amount:1010627717}, {n:3,name:'Chicken Store',amount:821293724}, {n:4,name:'Warehouse Block A',amount:2101686223}, {n:5,name:'Abattoir',amount:774577351}, {n:6,name:'Fermentation Shade',amount:1837836970}, {n:7,name:'Fertilizer Shade',amount:2510635190}, {n:8,name:'Capsules',amount:4193323976}, {n:9,name:'A-Tent',amount:4072148356}, {n:10,name:'Electricals',amount:2705389000}, {n:11,name:'External Works',amount:3378660053} ]; const BOQ_ELECTRICAL=[ {n:1,name:'Cold Brew',amount:2039810000}, {n:2,name:'Freeze Dry',amount:223855000}, {n:3,name:'Cosmetics',amount:92236000}, {n:4,name:'Extraction',amount:200883000}, {n:5,name:'Packaging',amount:156344000}, {n:6,name:'Administration Block',amount:272000000}, {n:7,name:'Spray Dry',amount:117936000}, {n:8,name:'Evaporator',amount:74173000}, {n:9,name:'Boiler 1',amount:60779000}, {n:10,name:'Boiler 2',amount:75660000}, {n:11,name:'Roastery',amount:178997000}, {n:12,name:'Wood Shed 1',amount:97415000}, {n:13,name:'Dry and Wet Mill-Upper',amount:311622000}, {n:14,name:'Dry Mill-Lower',amount:375987000}, {n:15,name:'Wet Mill-Lower',amount:303179000}, {n:16,name:'Silos and Dryer-Upper',amount:207803000}, {n:17,name:'Silos and Dryer-Lower',amount:185185000}, {n:18,name:'Gates and Coffee Shops',amount:313370000}, {n:19,name:'Exhibition',amount:627300000}, {n:20,name:'Staff Houses-Block A',amount:278760000}, {n:21,name:'Store 1 and 2',amount:596529000}, {n:22,name:'Water Treat Plant',amount:177307000}, {n:23,name:'Academy',amount:37990000}, {n:24,name:'Convention',amount:37990000}, {n:25,name:'Main Power Supply',amount:6476516000}, {n:26,name:'Security System',amount:279250000} ]; const BOQ_BUILDING_DETAIL={ 'Admin, Roastry & Cosmetics':[ {desc:'General Conditions & Preliminaries',amount:250000000}, {desc:'Site Preparation',amount:1644226048}, {desc:'Lower Ground Floor 2',amount:854860044}, {desc:'Lower Ground Floor 1',amount:898861078}, {desc:'Ground Floor',amount:2317209103}, {desc:'First Floor',amount:860896240}, {desc:'Structural Steel Works',amount:2702329558}, {desc:'Access Ramp',amount:284011630} ], 'Staff Accommodation Blocks':[ {desc:'G.C.C and Preliminaries',amount:274800000}, {desc:'Accommodation Block A',amount:644138668}, {desc:'Accommodation Block B',amount:1181850813}, {desc:'Accommodation Block C',amount:1450983202} ], 'Convention Centre':[ {desc:'G.C.C and Preliminaries',amount:150000000}, {desc:'Ground Floor 1',amount:1160937119}, {desc:'Lower Level 2',amount:951164597}, {desc:'Lower Level 1',amount:1717125640}, {desc:'Mezzanine Floor',amount:1352501779} ], 'Boiler 1 Building':[ {desc:'General Conditions & Preliminaries',amount:110000000}, {desc:'Site Preparation',amount:204386550}, {desc:'Warehouses',amount:183416800}, {desc:'Structural Steel Works',amount:409085529}, {desc:'Access Ramp',amount:5038750}, {desc:'External Works',amount:45250000} ], 'Boiler 2 Building':[ {desc:'General Conditions & Preliminaries',amount:110000000}, {desc:'Site Preparation',amount:481926000}, {desc:'Warehouses',amount:118424600}, {desc:'Structural Steel Works',amount:269578213}, {desc:'Access Ramp',amount:5038750}, {desc:'External Works',amount:26640000} ], 'Woodshade Warehouse Building 01':[ {desc:'General Conditions & Preliminaries',amount:120000000}, {desc:'Site Preparation',amount:209437500}, {desc:'Warehouses',amount:226355500}, {desc:'Structural Steel Works',amount:464618549}, {desc:'Access Ramp',amount:5038750}, {desc:'External Works',amount:60330000} ], 'Wood Shade Warehouse Bldg 2 (w/ Basement)':[ {desc:'General Conditions & Preliminaries',amount:120000000}, {desc:'Site Preparation',amount:233637500}, {desc:'Warehouses',amount:804203000}, {desc:'Structural Steel Works',amount:962664395}, {desc:'Access Ramp',amount:5038750}, {desc:'External Works',amount:60330000} ], 'Frezy Dry & Cold Brew Building':[ {desc:'General Conditions & Preliminaries',amount:185500000}, {desc:'Site Preparation',amount:1116582600}, {desc:'Warehouses',amount:1816564000}, {desc:'Structural Steel Works',amount:1293405048}, {desc:'Access Ramp',amount:6046750}, {desc:'External Works',amount:60330000} ], 'Spray Dryer Building':[ {desc:'General Conditions & Preliminaries',amount:165500000}, {desc:'Site Preparation',amount:63317800}, {desc:'Warehouses',amount:516974100}, {desc:'Structural Steel Works',amount:4371209629}, {desc:'Access Ramp',amount:5038750}, {desc:'External Works',amount:60330000} ], 'Spray Dryer Building Extension':[ {desc:'General Conditions & Preliminaries',amount:120000000}, {desc:'Site Preparation',amount:63317800}, {desc:'Warehouses',amount:516974100}, {desc:'Structural Steel Works',amount:4371209629}, {desc:'Access Ramp',amount:5038750}, {desc:'External Works',amount:60330000} ], 'Evaporator Building':[ {desc:'General Conditions & Preliminaries',amount:135500000}, {desc:'Site Preparation',amount:12312500}, {desc:'Warehouses',amount:83344500}, {desc:'Structural Steel Works',amount:941673545}, {desc:'Access Ramp',amount:5038750}, {desc:'External Works',amount:26987500} ], 'Wet & Dry Mill Building':[ {desc:'General Conditions & Preliminaries',amount:185500000}, {desc:'Site Preparation',amount:178937500}, {desc:'Warehouses',amount:3183506500}, {desc:'Structural Steel Works',amount:3962400629}, {desc:'Access Ramp',amount:5038750} ], 'Coffee Academy':[ {desc:'Preliminaries',amount:158000000}, {desc:'Coffee Academy',amount:14220424329} ], 'Agricultural Offices':[ {desc:'General Conditions & Preliminaries',amount:25000000}, {desc:'Site Preparation',amount:19207000}, {desc:'Generator House',amount:149773060}, {desc:'Structural Steel Works',amount:63398164}, {desc:'External Works',amount:128518300} ], 'Batch Plant':[ {desc:'General Conditions & Preliminaries',amount:25000000}, {desc:'Site Preparation',amount:96217616}, {desc:'Batch Plant',amount:82612960}, {desc:'Structural Steel Works',amount:65871200}, {desc:'External Works',amount:24033750} ], 'Cement Storage Structure':[ {desc:'General Conditions & Preliminaries',amount:25000000}, {desc:'Site Preparation',amount:181633221}, {desc:'Cement Storage',amount:400062798}, {desc:'Structural Steel Works',amount:432698030}, {desc:'External Works',amount:79752100} ], 'Conference Hall':[ {desc:'General Conditions & Preliminaries',amount:25000000}, {desc:'Site Preparation',amount:19207000}, {desc:'Generator House',amount:154637560}, {desc:'Structural Steel Works',amount:63398164}, {desc:'External Works',amount:128518300} ], 'Generator House':[ {desc:'General Conditions & Preliminaries',amount:25000000}, {desc:'Site Preparation',amount:138231806}, {desc:'Generator House',amount:137197930}, {desc:'Structural Steel Works',amount:126713868}, {desc:'External Works',amount:115158700} ], 'Extractor':[ {desc:'General Conditions & Preliminaries',amount:110000000}, {desc:'Site Preparation',amount:187471265}, {desc:'Warehouses',amount:271285408}, {desc:'Structural Steel Works',amount:773401222} ], 'Store 01 & 02':[ {desc:'General Conditions & Preliminaries',amount:243500000}, {desc:'Site Preparation',amount:257887435}, {desc:'Warehouses',amount:1360111866}, {desc:'Structural Steel Works',amount:925520063}, {desc:'Access Ramp',amount:6843000} ] }; function renderBoqCoffeePark(){ const tbody=document.getElementById('boq-cp-tbody'); const tfoot=document.getElementById('boq-cp-tfoot'); if(!tbody)return; const bill1Total=121885287637; // Merge static data with any user overrides saved in localStorage const overrides=JSON.parse(localStorage.getItem('erp_boq_overrides')||'{}'); const rows=BOQ_COFFEE_PARK.map((r,i)=>{ const row=overrides['cp_'+i]?{...r,...overrides['cp_'+i]}:r; const pct_=(row.sub/bill1Total*100).toFixed(1); const hasGrand=row.grand>0; return ` ${row.n} ${row.name}${row.note?'
'+row.note+'':''} ${row.sub.toLocaleString()} ${row.cont?row.cont.toLocaleString():'—'} ${hasGrand?row.grand.toLocaleString():'TBC'}
${pct_}% `; }); tbody.innerHTML=rows.join(''); if(tfoot)tfoot.innerHTML=`BILL 1 SUB-TOTAL121,885,287,637`; } function renderBoqNyabihoko(){ const tbody=document.getElementById('boq-ny-tbody'); const tfoot=document.getElementById('boq-ny-tfoot'); if(!tbody)return; const total=25762367144; const overrides=JSON.parse(localStorage.getItem('erp_boq_overrides')||'{}'); tbody.innerHTML=BOQ_NYABIHOKO.map((r,i)=>{ const row=overrides['ny_'+i]?{...r,...overrides['ny_'+i]}:r; const pct_=(row.amount/total*100).toFixed(1); const cont=row.cont||(row.amount?Math.round(row.amount*0.05):0); return ` ${row.n}${row.name} ${row.amount.toLocaleString()} ${cont.toLocaleString()} ——
${pct_}% `; }).join(''); if(tfoot)tfoot.innerHTML=`SUB TOTAL25,762,367,1441,288,118,3574,869,087,39031,919,572,891100%`; } function renderBoqElectrical(){ const tbody=document.getElementById('boq-elec-tbody'); const tfoot=document.getElementById('boq-elec-tfoot'); if(!tbody)return; const total=13798876000; tbody.innerHTML=BOQ_ELECTRICAL.map(r=>{ const pct_=(r.amount/total*100).toFixed(1); return `${r.n}${r.name}${r.amount.toLocaleString()}
${pct_}%`; }).join(''); tfoot.innerHTML=`TOTAL ELECTRICAL BILL13,798,876,000`; } function populateBuildingSelect(){ ['boq-building-select','boq-bldg-select'].forEach(id=>{ const sel=document.getElementById(id); if(!sel)return; const buildings=Object.keys(BOQ_BUILDING_DETAIL); sel.innerHTML=''+buildings.map(b=>``).join(''); }); } function renderBuildingDetail(){ const sel=document.getElementById('boq-building-select'); const div=document.getElementById('boq-building-detail-content'); if(!sel||!div)return; const name=sel.value; if(!name){div.innerHTML='';return;} const items=BOQ_BUILDING_DETAIL[name]; if(!items){div.innerHTML='

Detailed breakdown not available for this building.

';return;} const total=items.reduce((s,i)=>s+i.amount,0); const rows=items.map((item,i)=>` ${i+1} ${item.desc} ${item.amount.toLocaleString()}
${(item.amount/total*100).toFixed(1)}% `).join(''); const matched=BOQ_COFFEE_PARK.find(b=>b.name===name); div.innerHTML=`
${rows} ${matched&&matched.cont?``:''} ${matched&&matched.grand?``:''}
#Bill ElementAmount (UGX)% of Building Total
SUB-TOTAL${total.toLocaleString()}
+ Contingency 5%${matched.cont.toLocaleString()}
GRAND TOTAL (incl. VAT 18%)${matched.grand.toLocaleString()}
`; } /** Edit a BOQ Coffee Park (bill=cp) or Nyabihoko (bill=ny) row. * Saves override to erp_boq_overrides keyed as bill+'_'+rowIndex */ function openBoqCpRowModal(bill, rowIdx){ const overrides=JSON.parse(localStorage.getItem('erp_boq_overrides')||'{}'); const key=bill+'_'+rowIdx; const base=bill==='cp'?BOQ_COFFEE_PARK[rowIdx]:BOQ_NYABIHOKO[rowIdx]; if(!base){alert('Row not found.');return;} const current=overrides[key]||{}; const name=prompt('Building name:',current.name||base.name||''); if(name===null)return; const subField=bill==='cp'?'sub':'amount'; const subVal=parseInt(prompt('Sub-total amount (UGX, numbers only):',current[subField]||base[subField]||0)); if(isNaN(subVal)){alert('Invalid amount.');return;} let entry={name,notes:current.notes||''}; entry[subField]=subVal; if(bill==='cp'){ const cont=parseInt(prompt('Contingency (UGX):',current.cont||base.cont||Math.round(subVal*0.05))); const grand=parseInt(prompt('Grand Total incl. VAT (UGX):',current.grand||base.grand||0)); if(!isNaN(cont))entry.cont=cont; if(!isNaN(grand))entry.grand=grand; } overrides[key]=entry; localStorage.setItem('erp_boq_overrides',JSON.stringify(overrides)); if(bill==='cp')renderBoqCoffeePark(); else renderBoqNyabihoko(); } /** Alias wired to boq-bldg-select / boq-bldg-detail in the Building Details sub-panel */ function renderBoqBuildingDetail(){ const sel=document.getElementById('boq-bldg-select'); const div=document.getElementById('boq-bldg-detail'); if(!sel||!div)return; const name=sel.value; if(!name){div.innerHTML='

Select a building above to view its detailed Bill of Quantities breakdown.

';return;} const overrides=JSON.parse(localStorage.getItem('erp_boq_detail_overrides')||'{}'); const baseItems=BOQ_BUILDING_DETAIL[name]; if(!baseItems||!baseItems.length){div.innerHTML='

Detailed breakdown not available for this building.

';return;} // Merge per-item overrides (keyed by building__index) for existing rows const items=baseItems.map((item,i)=>{ const key=name+'__'+i; return overrides[key]&&!overrides[key].isCustom?{...item,...overrides[key]}:item; }); // Append custom items added via Add BOQ Item modal (isCustom:true, stored as array under building name) const customItems=(overrides[name]||[]).filter(ci=>ci.isCustom); const allItems=[...items,...customItems]; const total=allItems.reduce((s,i)=>s+(i.amount||0),0); const matched=BOQ_COFFEE_PARK.find(b=>b.name===name)||{}; const safeName=name.replace(/'/g,"\\'"); const rows=items.map((item,i)=>` ${i+1}${item.desc} ${(item.amount||0).toLocaleString()}
${total?((item.amount||0)/total*100).toFixed(1):0}% `).join(''); const customRows=customItems.map((item,ci)=>` ${items.length+ci+1}${item.desc} (Added) ${(item.amount||0).toLocaleString()}
${total?((item.amount||0)/total*100).toFixed(1):0}% `).join(''); div.innerHTML=`
${name} — IAG/COFFEE PARK BOQS | QS: Frank Odoi & Associates
${rows}${customRows} ${matched.cont?``:''} ${matched.grand?``:''}
#Bill Element / DescriptionAmount (UGX)% of TotalActions
Sub-Total${total.toLocaleString()}
+ Contingency (5%)${matched.cont.toLocaleString()}
Grand Total (incl. VAT 18%)${matched.grand.toLocaleString()}
`; } /** Print a building BoQ in a clean print layout */ function printBuildingBoq(name){ const overrides=JSON.parse(localStorage.getItem('erp_boq_detail_overrides')||'{}'); const baseItems=BOQ_BUILDING_DETAIL[name]; if(!baseItems)return; const items=baseItems.map((item,i)=>{const key=name+'__'+i;return overrides[key]?{...item,...overrides[key]}:item;}); const total=items.reduce((s,i)=>s+i.amount,0); const matched=BOQ_COFFEE_PARK.find(b=>b.name===name)||{}; const rows=items.map((item,i)=>`${i+1}${item.desc}${item.amount.toLocaleString()}${(item.amount/total*100).toFixed(1)}%`).join(''); const w=window.open('','_blank','width=900,height=700'); w.document.write(`BoQ — ${name}

Inspire Africa Coffee Park — Bill of Quantities

Building: ${name} | QS: Frank Odoi & Associates | Ref: IAG/COFFEE PARK BOQS

${rows} ${matched.cont?``:''} ${matched.grand?``:''}
#Bill Element / DescriptionAmount (UGX)% of Total
Sub-Total${total.toLocaleString()}100%
+ Contingency (5%)${matched.cont.toLocaleString()}
Grand Total (incl. VAT 18%)${matched.grand.toLocaleString()}

Printed: ${new Date().toLocaleString('en-UG')} | Inspire Africa Coffee Park, Rwashameire, Ntungamo District, Uganda

`); w.document.close(); w.focus(); setTimeout(()=>w.print(),500); } /** Open edit modal for a BOQ building detail item (bill element row) */ function openBoqDetailItemModal(buildingName, itemIdx){ const overrides=JSON.parse(localStorage.getItem('erp_boq_detail_overrides')||'{}'); const key=buildingName+'__'+itemIdx; const base=(BOQ_BUILDING_DETAIL[buildingName]||[])[itemIdx]||{}; const current=overrides[key]||base; // Reuse a simple prompt-based approach or inline modal const desc=prompt('Edit description:',current.desc||''); if(desc===null)return; // cancelled const amt=parseInt(prompt('Edit amount (UGX, numbers only):',current.amount)||current.amount); if(isNaN(amt)){alert('Invalid amount.');return;} overrides[key]={desc,amount:amt}; localStorage.setItem('erp_boq_detail_overrides',JSON.stringify(overrides)); renderBoqBuildingDetail(); } function renderQSStatus(){ const cpItems=[ {n:1,name:'Admin, Roastry and Cosmetics',sub:9812393701,grand:12157555796,status:'Complete'}, {n:2,name:'Staff Accommodation Blocks',sub:3551772684,grand:4400646355,status:'Complete'}, {n:3,name:'Convention Centre',sub:5331729133,grand:6606012396,status:'Complete'}, {n:4,name:'Boiler 1 Building',sub:957177629,grand:1185943082,status:'Complete'}, {n:5,name:'Boiler 2 Building',sub:1011607563,grand:1253381771,status:'Complete'}, {n:6,name:'Woodshade Warehouse Building 01',sub:1085780299,grand:1345281790,status:'Complete'}, {n:7,name:'Wood Shade Warehouse Building 2 (With Basement Storage)',sub:2185873645,grand:2708297446,status:'Complete'}, {n:8,name:'Frezy Dry & Cold Brew Building',sub:4478428398,grand:5548772785,status:'Complete'}, {n:9,name:'Spray Dryer Building',sub:5182370279,grand:6420956775,status:'Complete'}, {n:10,name:'Spray Dryer Building Extension',sub:5136870279,grand:6364582275,status:'Complete'}, {n:11,name:'Evaporator Building',sub:1204856795,grand:1492817569,status:'Complete'}, {n:12,name:'Wet & Dry Mill Building',sub:7515383379,grand:9311560006,status:'Complete'}, {n:13,name:'Coffee Academy',sub:14378424329,grand:17814867744,status:'Complete'}, {n:14,name:'Agricultural Offices',sub:385896524,grand:478125794,status:'Complete'}, {n:15,name:'Batch Plant',sub:293735526,grand:363938317,status:'Complete'}, {n:16,name:'Cement Storage Structure',sub:1119146149,grand:1386622078,status:'Complete'}, {n:17,name:'Conference Hall',sub:390761024,grand:484152909,status:'Complete'}, {n:18,name:'Generator House',sub:542302304,grand:671912555,status:'Complete'}, {n:19,name:'Electrical Installations',sub:13798876000,grand:0,status:'Complete'}, {n:20,name:'Extractor',sub:1342157895,grand:1662933632,status:'Complete'}, {n:21,name:'Store 01 & 02',sub:2793862363,grand:3461595468,status:'Complete'}, {n:22,name:'Raw Materials and Finished Goods',sub:5434556570,grand:0,status:'Pending'}, {n:23,name:'External Works',sub:33951325169,grand:0,status:'Pending'}, {n:24,name:'View Point',sub:0,grand:0,status:'Pending'}, {n:25,name:'Tents',sub:0,grand:0,status:'Pending'}, {n:26,name:'Packaging',sub:0,grand:0,status:'Pending'}, {n:27,name:'Wet Mill',sub:0,grand:0,status:'Pending'}, {n:28,name:'Dry Mill',sub:0,grand:0,status:'Pending'}, {n:29,name:'Silos',sub:0,grand:0,status:'Pending'} ]; const nyItems=[ {n:1,name:'Cattle Shade',sub:2356188584,grand:0,status:'Complete'}, {n:2,name:'Chicken Shade',sub:1010627717,grand:0,status:'Complete'}, {n:3,name:'Chicken Store',sub:821293724,grand:0,status:'Complete'}, {n:4,name:'Warehouse Block A',sub:2101686223,grand:0,status:'Complete'}, {n:5,name:'Abattoir',sub:774577351,grand:0,status:'Complete'}, {n:6,name:'Fermentation Shade',sub:1837836970,grand:0,status:'Complete'}, {n:7,name:'Fertilizer Shade',sub:2510635190,grand:0,status:'Complete'}, {n:8,name:'Capsules',sub:4193323976,grand:0,status:'Complete'}, {n:9,name:'A-Tent',sub:4072148356,grand:0,status:'Complete'}, {n:10,name:'Electricals',sub:2705389000,grand:0,status:'Complete'}, {n:11,name:'External Works',sub:3378660053,grand:0,status:'Complete'}, {n:12,name:'Tent',sub:0,grand:0,status:'Pending'}, {n:13,name:'Milk Freez Dry',sub:0,grand:0,status:'Pending'}, {n:14,name:'Old Cattle Shades',sub:0,grand:0,status:'Pending'}, {n:15,name:'Kitchen & Toilets',sub:0,grand:0,status:'Pending'} ]; const cpTbody=document.getElementById('qs-cp-tbody'); const nyTbody=document.getElementById('qs-ny-tbody'); if(cpTbody) cpTbody.innerHTML=cpItems.map(r=>` ${r.n} ${r.name} ${r.sub?r.sub.toLocaleString():'—'} ${r.grand?r.grand.toLocaleString():'—'} ${r.status==='Complete'?'BOQ Complete ✓':'BOQ Pending'} `).join(''); if(nyTbody) nyTbody.innerHTML=nyItems.map(r=>` ${r.n} ${r.name} ${r.sub?r.sub.toLocaleString():'—'} ${r.grand?r.grand.toLocaleString():'—'} ${r.status==='Complete'?'BOQ Complete ✓':'BOQ Pending'} `).join(''); } // ============================================================ // PROGRESS REPORT — MARCH 2026 // ============================================================ const PROGRESS_MAIN=[ {ref:'1a',contractor:'BAM',section:'Academy (Ground & 1st Floor)',pct:60,status:'Ongoing',achievement:'Ground floor near complete',remarks:'Needs BRC, sand, binding wire'}, {ref:'1b',contractor:'BAM',section:'Dispatch Area — Retaining Wall',pct:75,status:'Ongoing',achievement:'Base done, walls ongoing',remarks:'Storm water challenge'}, {ref:'1c',contractor:'BAM',section:'Cargo Lift — Staircase & Shaft',pct:40,status:'Ongoing',achievement:'Concrete works partial',remarks:'Drawing changes caused delays'}, {ref:'1d',contractor:'BAM',section:'Pavilion',pct:5,status:'Ongoing',achievement:'Earthworks commenced',remarks:'Excavator breakdowns'}, {ref:'1e',contractor:'BAM',section:'Concrete Batch Plant',pct:85,status:'Ongoing',achievement:'Installation nearing completion',remarks:'Lab compliance tests needed'}, {ref:'1f',contractor:'BAM',section:'Convention Centre (Lower Level Slab)',pct:36,status:'Halted',achievement:'—',remarks:'Halted pending consultant drawing issues'}, {ref:'1g',contractor:'FD Fan',section:'Boiler 1',pct:65,status:'Ongoing',achievement:'Rockfill done for platform',remarks:'Follow existing drawings carefully'}, {ref:'2a',contractor:'Matovu Engineering',section:'Proposed Dining Shade',pct:85,status:'Ongoing',achievement:'Floor slab & foundation done',remarks:'Needs blocks, sand, cement'}, {ref:'2b',contractor:'Matovu Engineering',section:"Consultant's Office",pct:85,status:'Ongoing',achievement:'Slab complete',remarks:'Needs blocks, sand, cement'}, {ref:'2c',contractor:'Matovu Engineering',section:'Agricultural Offices',pct:80,status:'Ongoing',achievement:'Excavations done',remarks:'Needs clay bricks, hardcore'}, {ref:'2d',contractor:'Matovu Engineering',section:'Storm Water Drains (Exhibition)',pct:75,status:'Ongoing',achievement:'Bases casted',remarks:'Consider slopes in finishes'}, {ref:'2e',contractor:'Matovu Engineering',section:'Main Gate',pct:90,status:'Ongoing',achievement:'Finishes & plastering',remarks:'Waterproof top of gate'}, {ref:'2f',contractor:'Matovu Engineering',section:'Cement Store',pct:60,status:'Ongoing',achievement:'Retaining wall base done',remarks:'Use strain iron bars'}, {ref:'3a',contractor:'Makoona',section:'Wood Sheds 1 & 2',pct:35,status:'Halted',achievement:'Frame panels & gutters on Shed 2',remarks:'Awaiting roofing materials'}, {ref:'3b',contractor:'Makoona',section:'Dining Sheds',pct:60,status:'Ongoing',achievement:'Z-purlins & braces installed',remarks:'Waiting for iron sheets'}, {ref:'3c',contractor:'Makoona',section:'Concrete Batch Plant',pct:80,status:'Ongoing',achievement:'Staircase & platform installed',remarks:'Apply paint on welded joints'}, {ref:'3d',contractor:'Makoona',section:'Cement Store, Security House, Consultant House',pct:0,status:'Pending',achievement:'—',remarks:'Waiting for substructure completion'}, {ref:'3e',contractor:'Makoona',section:'Production Zone',pct:90,status:'Ongoing',achievement:'Installation support ongoing',remarks:'Leakage defects in roofing'}, {ref:'4a',contractor:'Rurot',section:'Road Drainage, Manholes',pct:75,status:'Ongoing',achievement:'50m stormwater drain complete',remarks:'Drain level corrections needed'}, {ref:'5a',contractor:'Ptaka/Ssempala',section:'Retaining Walls & U-Drain',pct:70,status:'Ongoing',achievement:'Conveyor & silo columns',remarks:'Delayed payments affecting mobilisation'}, {ref:'6',contractor:'Sula',section:'Drainage & Stone Masonry (Link2 Road)',pct:65,status:'Ongoing',achievement:'Excavations ongoing',remarks:'Needs 12mm & 8mm steel'}, {ref:'7',contractor:'Alex Matua',section:'LPG Tank Base',pct:70,status:'Ongoing',achievement:'Excavation done, steel works underway',remarks:'Follow drawings carefully'}, {ref:'8',contractor:'Josira (Benjamin)',section:'Drainage & Stone Masonry (CEO Residence)',pct:65,status:'Ongoing',achievement:'Base & walls casted',remarks:'Focus on areas at risk from storm water'}, {ref:'9',contractor:'Ochaya',section:'Panel Cladding & Curtain Wall',pct:70,status:'Ongoing',achievement:'Aluminium at exhibition tent',remarks:'Needs financial support'}, {ref:'10',contractor:'Ramathan',section:'Caves, Moulding & Gates',pct:65,status:'Ongoing',achievement:'Stone works & plastering',remarks:'Provide quotation for restaurant slab'}, {ref:'11',contractor:'Eric (Ayinebwona)',section:'Stone Wall & Gabion Works',pct:70,status:'Ongoing',achievement:'Gabion below pitch',remarks:'Needs wheel loader for backfilling'}, {ref:'12a',contractor:'Ian Nowera',section:'Face Bricks (Perimeter & Tent)',pct:75,status:'Ongoing',achievement:'Face bricks at perimeter wall',remarks:'Improve quality of brick joints'}, {ref:'13',contractor:'Asiimwe',section:'Reinforced Retaining Walls (Dry Mill)',pct:69,status:'Ongoing',achievement:'Silo retaining wall steel works',remarks:'Needs 16mm & 12mm steel'}, {ref:'14a',contractor:'John Sonko',section:'Water Tank & Steel Tank Bases',pct:79,status:'Ongoing',achievement:'Ground base of tank stand casted',remarks:'Needs finance & material support'}, {ref:'15a',contractor:'Brian Iraka',section:'Retaining Wall Stone Masonry (Boiler 1)',pct:55,status:'Ongoing',achievement:'Columns partially complete',remarks:'Improve labour mobilisation'}, {ref:'16',contractor:'Job Taremwa',section:'Retaining & Perimeter Walls (below Boiler 1)',pct:25,status:'Halted',achievement:'—',remarks:'Awaiting consultant decision on ground plan'}, {ref:'17',contractor:'Kazoora',section:'Cross Culverts & Kerb Stones',pct:69,status:'Ongoing',achievement:'900mm culvert ongoing',remarks:'Low mobilisation, needs finance'}, {ref:'18',contractor:'Steel & Tube',section:'Roofing at Dry & Wet Mill',pct:90,status:'Inactive',achievement:'Roofing & iron sheet cladding',remarks:'Leaking gutters, wrong orientation'}, {ref:'19',contractor:'Uganda Baati',section:'Roofing in Production Zone & Dispatch',pct:85,status:'Inactive',achievement:'Dispatch roofing done',remarks:'Leakage defects from poor downpipe placement'} ]; const PROGRESS_NYABIHOKO=[ {ref:'1a',contractor:'Nowera',section:'Freeze Dry — Roof Works',pct:70,status:'Ongoing',remarks:'Rains & power issues slowing progress'}, {ref:'1b',contractor:'Nowera',section:'Fertilizer Shed — Retaining Wall',pct:50,status:'Halted',remarks:'Needs more hardcore'}, {ref:'1c',contractor:'Nowera',section:'Abattoir',pct:95,status:'Ongoing',remarks:'Block work & columns complete, plastering ongoing'}, {ref:'1d',contractor:'Nowera',section:'Kitchen Lakeside',pct:65,status:'On Hold',remarks:'Awaiting consultant (Mr Yassin) approval'}, {ref:'1e',contractor:'Nowera',section:'Tent Lakeside',pct:10,status:'On Hold',remarks:'Awaiting consultant approval'}, {ref:'1f',contractor:'Nowera',section:'Street Lights',pct:75,status:'Ongoing',remarks:'Foundation casting near complete'}, {ref:'2a',contractor:'Nowera & Inspire',section:'Capsule Houses (Units 3-9)',pct:60,status:'In Progress',remarks:'Glass & roof installation ongoing'}, {ref:'2b',contractor:'Nowera & Inspire',section:'A-Shaped Houses (Units 6-10)',pct:50,status:'In Progress',remarks:'Halted at units 9 & 10'}, {ref:'3',contractor:'Donic & Ruroti',section:'Cattle Shed & Warehouse',pct:0,status:'On Hold',remarks:'Handed over to Ruroti & Donic'} ]; function progressStatusBadge(s){ const m={'Ongoing':'badge-green','Halted':'badge-amber','Pending':'badge-red','Inactive':'badge-red','On Hold':'badge-blue','In Progress':'badge-teal'}; return `${s}`; } // getProgressMain / getProgressNyabihoko / saveProgressMain / saveProgressNyabihoko // are defined further below (alongside openProgressEntryModal) function renderProgressMain(){ const tbody=document.getElementById('progress-main-tbody'); if(!tbody)return; const data = getProgressMain(); tbody.innerHTML=data.map((r,i)=>{ const pc=progClass(r.pct); return ` ${r.ref} ${r.contractor} ${r.section}
${r.pct}% ${progressStatusBadge(r.status)} ${r.achievement||'—'} ${r.remarks} `; }).join(''); } function renderProgressNyabihoko(){ const tbody=document.getElementById('progress-nyab-tbody')||document.getElementById('progress-ny-tbody'); if(!tbody)return; const data = getProgressNyabihoko(); tbody.innerHTML=data.map((r,i)=>{ const pc=progClass(r.pct); return ` ${r.ref} ${r.contractor} ${r.section}
${r.pct}% ${progressStatusBadge(r.status)} ${r.remarks} `; }).join(''); } function renderProgressChallenges(){ const el=document.getElementById('progress-challenges-content'); if(!el)return; const challenges=[ {icon:'⚠️',title:'Low Contractor Mobilisation',desc:'37 active contractors but average only 15 present on site daily. Mobilisation dropped from 500+ to under 200 workers per day.'}, {icon:'🧱',title:'Technical Competence Gaps',desc:'Contractors lacking technical competence — not using concrete mixers, vibrators, or gauge boxes. Works being done without proper approved drawings.'}, {icon:'📦',title:'Delayed Material Procurement',desc:'Critical shortage of culverts, BRC mesh, steel bars (12mm, 16mm), roofing iron sheets. Material delays are causing cascading construction halts.'}, {icon:'🏗️',title:'Block Production Deficit',desc:'Only 968 blocks produced per month against the site requirement. At current rate, block supply is severely insufficient for structural works.'}, {icon:'🏔️',title:'Storm Water Management',desc:'Storm water challenges affecting multiple zones including retaining walls, dispatch area, and CEO residence drainage. Slope corrections needed in finishes.'}, {icon:'💰',title:'Financial Constraints',desc:'Multiple contractors (Ptaka/Ssempala, Kazoora, others) reporting that delayed payments are directly reducing their on-site labour mobilisation.'}, {icon:'📐',title:'Drawing & Design Issues',desc:'Convention Centre slab halted pending consultant drawing issues. Cargo lift shaft delayed due to drawing changes. Works proceeding without approved drawings in some areas.'}, {icon:'🔧',title:'Equipment Breakdowns',desc:'Excavator breakdowns delaying Pavilion earthworks. Poor quality sand and aggregate from suppliers affecting concrete quality.'} ]; el.innerHTML=challenges.map(c=>`
${c.icon}
${c.title}${c.desc}
`).join(''); } function renderProgressToolbox(){ const el=document.getElementById('progress-toolbox-content'); if(!el)return; el.innerHTML=`

Contractor Performance Issues — March 2026 Toolbox

Site Attendance37 contractors registered / avg 15 present daily
Labour MobilisationDropped from 500+ to below 200 workers/day
Drawing ComplianceMultiple contractors working without approved drawings
Equipment UsageMixers, vibrators, and gauge boxes not being used
Block Production968 blocks/month (severely insufficient)
Material QualityPoor quality sand and aggregate from suppliers
Defect RateRoofing leakages at Production Zone, Dry & Wet Mill
Action RequiredToolbox meeting to enforce drawing compliance, PPE, mixer usage

Halted Works — Awaiting Decision / Materials

Convention Centre Lower SlabBAM — Halted, consultant drawing issue
Wood Shed 1 & 2Makoona — Halted, awaiting roofing materials
Cement Store / Security / Consultant HouseMakoona — Pending substructure completion
Retaining Wall below Boiler 1Job Taremwa — Halted, awaiting ground plan decision
Fertilizer Shed Retaining Wall (Nyabihoko)Nowera — Halted, needs hardcore
`; } // ============================================================ // PAYMENTS MODULE // ============================================================ const REAL_PAYMENTS=[ {n:1,name:'Tashobya Wens',contract:116000000,paid:60000000,balance:56000000,requested:40000000,proposed:30000000}, {n:2,name:'Talemwa Job — Wood Shed',contract:130980000,paid:98000000,balance:32980000,requested:32000000,proposed:20000000}, {n:3,name:'Home Designs Fowers — Kizito',contract:906406242,paid:664850000,balance:140000000,requested:100000000,proposed:80000000}, {n:4,name:'Sonko John Baptist — Upper Tank',contract:30000000,paid:22000000,balance:8000000,requested:8000000,proposed:7200000}, {n:5,name:'Sonko John Baptist — Main Tank',contract:22000000,paid:10000000,balance:12000000,requested:2000000,proposed:1800000}, {n:6,name:'Matovu Vincent Engineering',contract:812070000,paid:245000000,balance:567070000,requested:237000000,proposed:90000000}, {n:7,name:'Rurot Engineering (Old)',contract:450450000,paid:391000000,balance:59450000,requested:30000000,proposed:20000000}, {n:8,name:'Rurot Engineering (New Admin)',contract:102005000,paid:0,balance:102005000,requested:30000000,proposed:20000000}, {n:9,name:'Hirya Ramadthan',contract:698000000,paid:270000000,balance:428000000,requested:191000000,proposed:50000000}, {n:10,name:'Kyoga Mazinga',contract:211028250,paid:112950000,balance:98078250,requested:30000000,proposed:15000000}, {n:11,name:'Arafat Block Production',contract:6084000,paid:1000000,balance:5084000,requested:5840000,proposed:5840000}, {n:12,name:'Asiimwe Emmy — Mwinos',contract:150000000,paid:25000000,balance:125000000,requested:39000000,proposed:20000000}, {n:13,name:'Ainebyona Investments',contract:164680000,paid:70000000,balance:94680000,requested:50000000,proposed:40000000}, {n:14,name:'Jumbe Construction',contract:148600000,paid:70400000,balance:78200000,requested:33840000,proposed:10000000}, {n:15,name:'Maurice Omollo — Nyabihoko',contract:230000000,paid:116000000,balance:114000000,requested:50000000,proposed:20000000}, {n:16,name:'Maurice Omollo — Exhibition Area',contract:43000000,paid:10000000,balance:33000000,requested:5000000,proposed:2000000}, {n:17,name:'Iraka Brian',contract:216632400,paid:201000000,balance:15632400,requested:24000000,proposed:20000000}, {n:18,name:'Kyambogo Ian — Face Brick',contract:22000000,paid:8000000,balance:14000000,requested:17000000,proposed:13000000}, {n:19,name:'Arinaitwe David',contract:111000000,paid:78000000,balance:33000000,requested:25000000,proposed:20000000}, {n:20,name:'Makoona Musa',contract:529000000,paid:341500000,balance:187500000,requested:81500000,proposed:50000000}, {n:21,name:'Sigi Patrick Electricals',contract:132339000,paid:66500000,balance:65839000,requested:10000000,proposed:10000000}, {n:22,name:'Alex Matwa — Extraction Powerhouse',contract:58764000,paid:20000000,balance:38764000,requested:30000000,proposed:20000000}, {n:23,name:'Wambogo Samson — Solar Drier/Wet Mill',contract:25000000,paid:18000000,balance:7000000,requested:11000000,proposed:8000000}, {n:24,name:'Wambogo Samson — Solar Drier New',contract:15000000,paid:5000000,balance:10000000,requested:5000000,proposed:3000000}, {n:25,name:'Allan Nkabahita — Tiles/Terrazzo/Guest House',contract:230323000,paid:178272000,balance:52051000,requested:20000000,proposed:10000000}, {n:26,name:'Kubariho Venture',contract:5000000,paid:0,balance:5000000,requested:5000000,proposed:5000000}, {n:27,name:'Mubajje Sulah',contract:33500000,paid:22500000,balance:11000000,requested:11000000,proposed:9000000}, {n:28,name:'Oloka Painter — ACP Office',contract:18300000,paid:8000000,balance:10300000,requested:7000000,proposed:5000000}, {n:29,name:'Oloka Painter — Exhibition & Labour',contract:8000000,paid:6000000,balance:2000000,requested:1000000,proposed:500000}, {n:30,name:'Oloka Painter — CEO Residence',contract:7000000,paid:1600000,balance:5400000,requested:3000000,proposed:1000000}, {n:31,name:'Ptaka Engineering (Sempala)',contract:434609500,paid:251000000,balance:183609500,requested:110000000,proposed:50000000}, {n:32,name:'Hana Express — Culvert Box',contract:95000000,paid:53000000,balance:42000000,requested:42000000,proposed:10000000}, {n:33,name:'Doascore Engineering — Donic Fountain',contract:131615000,paid:79500000,balance:52115000,requested:49000000,proposed:20000000}, {n:34,name:'Ndance Gabions',contract:82620000,paid:45000000,balance:37620000,requested:41000000,proposed:10000000}, {n:35,name:'Opoya Didas',contract:5000000,paid:3000000,balance:2000000,requested:2000000,proposed:1000000}, {n:36,name:'Mark Kilama',contract:168991000,paid:126400000,balance:42591000,requested:30000000,proposed:10000000}, {n:37,name:'Alex Carpenter',contract:14000000,paid:9500000,balance:4500000,requested:1500000,proposed:1500000}, {n:38,name:'Kwesiga Allan — Cleaning Feb-March',contract:5000000,paid:5000000,balance:0,requested:5000000,proposed:5000000}, {n:39,name:'Wilson Omalla — Reception/Restaurant/Deck',contract:35000000,paid:5000000,balance:30000000,requested:10000000,proposed:10000000}, {n:40,name:'Ochaya Godfrey — Cladding',contract:301000000,paid:205000000,balance:96000000,requested:30000000,proposed:20000000}, {n:41,name:'Jane Musharaba',contract:13129500,paid:13129500,balance:13000000,requested:13000000,proposed:13000000} ]; function renderPaymentTable(){ const tbody=document.getElementById('payments-tbody'); const tfoot=document.getElementById('payments-tfoot'); if(!tbody)return; const search=(document.getElementById('pay-search')||{}).value||''; const rows=REAL_PAYMENTS.filter(r=>!search||r.name.toLowerCase().includes(search.toLowerCase())); tbody.innerHTML=rows.map(r=>{ const paidPct=r.contract?Math.round(r.paid/r.contract*100):0; const balClass=r.balance<=0?'color:var(--green)':'color:var(--orange)'; return ` ${r.n} ${r.name} ${r.contract.toLocaleString()} ${r.paid.toLocaleString()}
${r.balance.toLocaleString()} ${r.requested.toLocaleString()} ${r.proposed.toLocaleString()} Pending CEO `; }).join(''); const totContract=REAL_PAYMENTS.reduce((s,r)=>s+r.contract,0); const totPaid=REAL_PAYMENTS.reduce((s,r)=>s+r.paid,0); const totBal=REAL_PAYMENTS.reduce((s,r)=>s+r.balance,0); const totReq=REAL_PAYMENTS.reduce((s,r)=>s+r.requested,0); const totProp=REAL_PAYMENTS.reduce((s,r)=>s+r.proposed,0); if(tfoot) tfoot.innerHTML=`TOTALS (41 Contractors)${totContract.toLocaleString()}${totPaid.toLocaleString()}${totBal.toLocaleString()}${totReq.toLocaleString()}${totProp.toLocaleString()}`; } function exportPaymentsCSV(){ const rows=[['#','Contractor','Contract Sum','Paid','Balance','Requested','Proposed']]; REAL_PAYMENTS.forEach(r=>rows.push([r.n,r.name,r.contract,r.paid,r.balance,r.requested,r.proposed])); const csv=rows.map(r=>r.map(c=>'"'+String(c).replace(/"/g,'""')+'"').join(',')).join('\n'); const blob=new Blob([csv],{type:'text/csv'}); const a=document.createElement('a');a.href=URL.createObjectURL(blob);a.download='payments-apr2026.csv';a.click(); } function renderPaymentAnalysis(){ const el=document.getElementById('payments-analysis-content'); if(!el)return; const totContract=REAL_PAYMENTS.reduce((s,r)=>s+r.contract,0); const totPaid=REAL_PAYMENTS.reduce((s,r)=>s+r.paid,0); const totBal=REAL_PAYMENTS.reduce((s,r)=>s+r.balance,0); const totProp=REAL_PAYMENTS.reduce((s,r)=>s+r.proposed,0); const highestProp=[...REAL_PAYMENTS].sort((a,b)=>b.proposed-a.proposed).slice(0,5); el.innerHTML=`
Total Contract Value
UGX ${totContract.toLocaleString()}
41 contractors
Total Paid to Date
UGX ${totPaid.toLocaleString()}
${Math.round(totPaid/totContract*100)}% of contracts
Total Outstanding
UGX ${totBal.toLocaleString()}
Proposed April 2026
UGX ${totProp.toLocaleString()}
Top 5 Proposed April Payments
${highestProp.map((r,i)=>`
${i+1}. ${r.name}UGX ${r.proposed.toLocaleString()}
`).join('')}
`; } function renderPaymentCertificate(){ const el=document.getElementById('payment-certificate-content'); if(!el)return; const totProp=REAL_PAYMENTS.reduce((s,r)=>s+r.proposed,0); el.innerHTML=`
INSPIRE AFRICA COFFEE PARK
Rwashameire, Ntungamo District, Uganda
CONTRACTOR PAYMENT CERTIFICATE
Certificate No: IACP/PAY/CERT/APR/2026/001
Certificate DateApril 2026
Payment Plan ReferenceIACP/PPL/2026/Q2
No. of Contractors41
Total Contract Value (Cumulative)UGX 6,919,126,892
Previously Certified & PaidUGX 3,916,101,500
This Certificate — Proposed PaymentUGX ${totProp.toLocaleString()}
This payment certificate is issued subject to satisfactory progress verification and approval by the CEO. All payments shall be processed through the Finance Department and require dual authorization.
Head of Contractors
Nkabahita Allan
CEO / Authorizing Officer
Nelson Tugume
Finance Department
Hillary Obukui
Project Manager
Kubariho Venture
`; } // ============================================================ // FUNCTION ALIASES & COMPATIBILITY SHIMS // The HTML panels use these names; the implementations above use // slightly different names — these aliases bridge the gap. // ============================================================ /** Called by refreshModule('boq') — runs all BOQ sub-render functions */ function initBoqModule(){ renderBoqCoffeePark(); renderBoqNyabihoko(); renderBoqElectrical(); populateBuildingSelect(); renderQSStatus(); // Populate cert-contractor dropdown if not already populated populateCertContractorDropdown(); } /** Called by oninput on pr-search and onchange on pr-status (progressrpt panel) */ function renderProgressReport(){ // Render main site with optional search/status filter — uses dynamic data const search=((document.getElementById('pr-search')||{}).value||'').toLowerCase(); const status=(document.getElementById('pr-status')||{}).value||''; const mainTbody=document.getElementById('progress-main-tbody'); if(mainTbody){ const all=getProgressMain(); const data=all.filter(r=>{ const ms=!search||(r.contractor.toLowerCase().includes(search)||r.section.toLowerCase().includes(search)); const st=!status||r.status===status; return ms&&st; }); mainTbody.innerHTML=data.map((r,i)=>{ const pc=progClass(r.pct); const origIdx=all.indexOf(r); return ` ${r.ref||r.no||''} ${r.contractor} ${r.section}
${r.pct||0}% ${progressStatusBadge(r.status)} ${r.achievement||'—'} ${r.workers||'—'} ${r.remarks||''} `; }).join(''); } // Render Nyabihoko const nyTbody=document.getElementById('progress-nyab-tbody'); if(nyTbody){ const nyAll=getProgressNyabihoko(); nyTbody.innerHTML=nyAll.map((r,i)=>` ${r.ref||r.no||''} ${r.contractor} ${r.section} ${progressStatusBadge(r.status)} ${r.achievement||'—'} ${r.remarks||''} `).join(''); } } /** Called by oninput on pay-search (payments panel) */ function renderPaymentsTable(){ renderPaymentTable(); } /** Called by initPaymentsModule in refreshModule('payments') */ function initPaymentsModule(){ populateCertContractorDropdown(); renderPaymentAnalysis(); renderPaymentCertificate(); } /** Populate the cert-contractor select in the Payment Certificate generator */ function populateCertContractorDropdown(){ const sel=document.getElementById('cert-contractor'); if(!sel||sel.options.length>1) return; REAL_PAYMENTS.forEach(r=>{ const o=document.createElement('option'); o.value=r.n; o.textContent=r.name; sel.appendChild(o); }); } /** Generate a payment certificate preview */ function generateCert(){ const sel=document.getElementById('cert-contractor'); const id=sel?parseInt(sel.value):0; const c=REAL_PAYMENTS.find(r=>r.n===id); if(!c){alert('Please select a contractor first.');return;} const amt=parseFloat((document.getElementById('cert-amount')||{}).value)||0; const retPct=parseFloat((document.getElementById('cert-retention')||{}).value)||5; const certNo=(document.getElementById('cert-no')||{}).value||'PC-001'; const from=(document.getElementById('cert-from')||{}).value||''; const to=(document.getElementById('cert-to')||{}).value||''; const retAmt=Math.round(amt*retPct/100); const net=amt-retAmt; const preview=document.getElementById('cert-preview'); if(!preview) return; preview.style.display='block'; preview.innerHTML=`
INSPIRE AFRICA COFFEE PARK
Rwashameire, Ntungamo District, Uganda
PAYMENT CERTIFICATE No. ${certNo}
Contractor${c.name}
Contract SumUGX ${c.contract.toLocaleString()}
Period${from||'—'} to ${to||'—'}
Previously CertifiedUGX ${c.paid.toLocaleString()}
Amount This CertificateUGX ${amt.toLocaleString()}
Retention (${retPct}%)UGX ${retAmt.toLocaleString()}
NET AMOUNT DUE UGX ${net.toLocaleString()}
Certified by: Site Engineer / QS
Approved by: CEO / PM
`; } /** Print payment certificate */ function printPayCert(){ window.print(); } // ============================================================ // ENHANCED MASTER CONTRACTOR MANAGEMENT // REAL_PAYMENTS is the single source of truth for 41 contractors. // On first load the master list is merged into localStorage so both // the Contractors tab and Payments tab read from the same array. // ============================================================ const IKEY_MASTER = 'erp_master_contractors'; /** Return the master contractor list — merges REAL_PAYMENTS with * any extended fields stored in localStorage. */ function getMasterContractors() { try { const stored = JSON.parse(localStorage.getItem(IKEY_MASTER)); if (stored && stored.length) return stored; } catch(e) {} // First run: build master from REAL_PAYMENTS seed const master = REAL_PAYMENTS.map(r => ({ n: r.n, name: r.name, company_type: 'Individual', discipline: guessDisc(r.name), contract_no: 'IACP/CONTR/2025/' + String(r.n).padStart(3,'0'), contract_date: '2025-01-01', contract_sum: r.contract, scope: '', site: 'Coffee Park (Main Site)', phone: '—', email: '', engineer: '', status: r.balance <= 0 ? 'Complete' : 'Active', notes: '', paid: r.paid, balance: r.balance, requested: r.requested, proposed: r.proposed, rating: 3, lastPaymentDate: '', paid_history: [], zonesAssigned: [] })); localStorage.setItem(IKEY_MASTER, JSON.stringify(master)); return master; } function guessDisc(name) { const n = name.toLowerCase(); if (n.includes('electric')) return 'Electrical'; if (n.includes('plumb')) return 'Plumbing / HVAC'; if (n.includes('steel') || n.includes('fabricat')) return 'Steel / Fabrication'; if (n.includes('paint')) return 'Painting'; if (n.includes('tiles') || n.includes('terrazzo') || n.includes('finish')) return 'Finishing / Tiles'; if (n.includes('roof') || n.includes('cladding')) return 'Roofing / Cladding'; if (n.includes('carp')) return 'Carpentry / Joinery'; if (n.includes('gabion')) return 'Gabion Works'; if (n.includes('culvert') || n.includes('drain')) return 'Culverts / Drainage'; if (n.includes('block')) return 'Block Making'; if (n.includes('retain') || n.includes('masonry') || n.includes('stone')) return 'Stone Masonry'; if (n.includes('cleaning')) return 'Site Cleaning'; if (n.includes('kubariho') || n.includes('venture')) return 'Project Management'; if (n.includes('brick')) return 'Face Brickwork'; return 'Civil Works'; } function saveMasterContractors(arr) { localStorage.setItem(IKEY_MASTER, JSON.stringify(arr)); } /** Contractor form — open for Add (idx = -1) or Edit */ function openContractorForm(idx) { const modal = document.getElementById('add-contractor-modal'); if (!modal) return; const titleEl = document.getElementById('ac-modal-title'); const idxEl = document.getElementById('ac-edit-idx'); idxEl.value = idx < 0 ? '' : idx; if (idx < 0) { // New contractor — clear form if (titleEl) titleEl.textContent = 'Add Contractor'; ['c-name','c-contact','c-email','c-engineer','c-contract-no','c-scope','c-notes'].forEach(id => {const el=document.getElementById(id);if(el)el.value='';}); const cd = document.getElementById('c-contract-date'); if(cd) cd.value = today(); const vEl = document.getElementById('c-value'); if(vEl) vEl.value = 0; const pEl = document.getElementById('c-paid'); if(pEl) pEl.value = 0; const stEl = document.getElementById('c-status'); if(stEl) stEl.value = 'Active'; const rtEl = document.getElementById('c-rating'); if(rtEl) rtEl.value = 3; const lp = document.getElementById('c-lastpay'); if(lp) lp.value = ''; } else { if (titleEl) titleEl.textContent = 'Edit Contractor'; const master = getMasterContractors(); const c = master[idx]; if (!c) return; const set = (id, val) => { const el=document.getElementById(id); if(el) el.value = val||''; }; set('c-name', c.name); set('c-company-type', c.company_type || 'Individual'); set('c-spec', c.discipline || c.spec || 'Civil Works'); set('c-contract-no', c.contract_no || ''); set('c-contract-date', c.contract_date || ''); set('c-value', c.contract_sum || c.value || 0); set('c-paid', c.paid || 0); set('c-site', c.site || 'Coffee Park (Main Site)'); set('c-status', c.status || 'Active'); set('c-contact', c.phone || c.contact || ''); set('c-email', c.email || ''); set('c-engineer', c.engineer || ''); set('c-rating', c.rating || 3); set('c-lastpay', c.lastPaymentDate || ''); set('c-scope', c.scope || ''); set('c-notes', c.notes || ''); } modal.classList.remove('hidden'); } function saveContractorForm() { const name = (document.getElementById('c-name')||{}).value.trim(); if (!name) return alert('Contractor name is required.'); const get = id => (document.getElementById(id)||{}).value || ''; const idx = document.getElementById('ac-edit-idx').value; const master = getMasterContractors(); const contractSum = +get('c-value') || 0; const paid = +get('c-paid') || 0; const entry = { name, company_type: get('c-company-type'), discipline: get('c-spec'), spec: get('c-spec'), contract_no: get('c-contract-no'), contract_date: get('c-contract-date'), contract_sum: contractSum, value: contractSum, scope: get('c-scope'), site: get('c-site'), phone: get('c-contact'), contact: get('c-contact'), email: get('c-email'), engineer: get('c-engineer'), status: get('c-status'), rating: +get('c-rating') || 3, notes: get('c-notes'), paid, balance: contractSum - paid, requested: 0, proposed: 0, lastPaymentDate: get('c-lastpay'), paid_history: [], zonesAssigned: [] }; if (idx === '') { entry.n = master.length + 1; master.push(entry); addActivity('Contractor added: ' + name); } else { const i = parseInt(idx); entry.n = master[i] ? master[i].n : i + 1; entry.paid_history = master[i] ? (master[i].paid_history || []) : []; entry.zonesAssigned = master[i] ? (master[i].zonesAssigned || []) : []; entry.requested = master[i] ? (master[i].requested || 0) : 0; entry.proposed = master[i] ? (master[i].proposed || 0) : 0; master[i] = entry; addActivity('Contractor updated: ' + name); } saveMasterContractors(master); // Also sync to legacy erp_contractors key so other code keeps working syncMasterToLegacy(master); closeModal('add-contractor-modal'); renderRealContractorList(); renderPaymentSchedule(); renderPaymentTable(); } function syncMasterToLegacy(master) { const legacy = master.map(c => ({ name: c.name, contact: c.phone || c.contact || '—', spec: c.discipline || c.spec || 'Civil Works', value: c.contract_sum || c.value || 0, paid: c.paid || 0, status: c.status || 'Active', rating: c.rating || 3, lastPaymentDate: c.lastPaymentDate || '', zonesAssigned: c.zonesAssigned || [] })); save('contractors', legacy); } // Override the old saveContractor to use unified form function saveContractor() { saveContractorForm(); } // Override editContractor to open unified form function editContractor(idx) { openContractorForm(idx); } // Override saveEditedContractor function saveEditedContractor() { saveContractorForm(); } // Rewrite renderRealContractorList to use master data function renderRealContractorListNew() { const master = getMasterContractors(); const search = ((document.getElementById('ct-search')||{}).value||'').toLowerCase(); const stF = (document.getElementById('ct-status-filter')||{}).value||''; const spF = (document.getElementById('ct-spec-filter')||{}).value||''; const tbody = document.getElementById('contractors-tbody'); if (!tbody) return; const filtered = master.filter(c => { if (search && !(c.name||'').toLowerCase().includes(search)) return false; if (stF && c.status !== stF) return false; if (spF && (c.discipline||c.spec) !== spF) return false; return true; }); tbody.innerHTML = filtered.map((c, i) => { const bal = (c.contract_sum || c.value || 0) - (c.paid || 0); const paidPct = (c.contract_sum || c.value) ? Math.round((c.paid||0)/(c.contract_sum||c.value||1)*100) : 0; const masterIdx = master.indexOf(c); return ` ${c.name}
${c.discipline||c.spec||'—'} ${c.phone||c.contact||'—'} ${c.contract_no||'—'} ${fmtShort(c.contract_sum||c.value||0)} ${fmtShort(c.paid||0)}
${fmtShort(bal)} ${statusBadge(c.status||'Active')} ${stars(c.rating||3)} ${c.site||'—'} ${c.lastPaymentDate||'—'} `; }).join(''); } function delMasterContractor(idx) { if (!confirm('Delete this contractor?')) return; const master = getMasterContractors(); master.splice(idx, 1); saveMasterContractors(master); syncMasterToLegacy(master); renderRealContractorListNew(); renderPaymentTable(); } function openPayModalForMaster(idx) { const master = getMasterContractors(); const c = master[idx]; if (!c) return; openModal('record-payment-modal'); setTimeout(() => { const sel = document.getElementById('pay-contractor'); if (sel) { for (let o of sel.options) if (o.text === c.name || o.value === c.name) { o.selected = true; break; } } const cumEl = document.getElementById('pay-cumulative'); if (cumEl) cumEl.value = c.paid || 0; }, 100); } // ============================================================ // ENHANCED PAYMENT RECORDING // ============================================================ function recalcPayModal() { const certified = parseFloat((document.getElementById('pay-amount')||{}).value) || 0; const retPct = parseFloat((document.getElementById('pay-ret-pct')||{}).value) || 5; const retAmt = Math.round(certified * retPct / 100); const net = certified - retAmt; const retEl = document.getElementById('pay-ret-amt'); if(retEl) retEl.value = retAmt; const netEl = document.getElementById('pay-net'); if(netEl) netEl.value = net; } function recalcCertForm() { const certified = parseFloat((document.getElementById('cert-amount')||{}).value) || 0; const retPct = parseFloat((document.getElementById('cert-retention')||{}).value) || 5; const retAmt = Math.round(certified * retPct / 100); const net = certified - retAmt; const retEl = document.getElementById('cert-ret-amt'); if(retEl) retEl.value = retAmt; const netEl = document.getElementById('cert-net'); if(netEl) netEl.value = net; } // Override recordPayment for enhanced version function recordPayment() { const ctrlName = (document.getElementById('pay-contractor')||{}).value; const amount = +(document.getElementById('pay-amount')||{}).value || 0; const date = (document.getElementById('pay-date')||{}).value; const method = (document.getElementById('pay-method')||{}).value; const ref = (document.getElementById('pay-ref')||{}).value; const by = (document.getElementById('pay-by')||{}).value; const narration = (document.getElementById('pay-narration')||{}).value; const retPct = +(document.getElementById('pay-ret-pct')||{}).value || 5; const gross = +(document.getElementById('pay-gross')||{}).value || amount; const net = +(document.getElementById('pay-net')||{}).value || (amount - Math.round(amount*retPct/100)); const periodFrom = (document.getElementById('pay-period-from')||{}).value; const periodTo = (document.getElementById('pay-period-to')||{}).value; const bankRef = (document.getElementById('pay-bank-ref')||{}).value; const certEng = (document.getElementById('pay-certifying-eng')||{}).value; if (!amount || !date) return alert('Date and amount are required.'); // Update master contractor paid amount const master = getMasterContractors(); const mi = master.findIndex(c => c.name === ctrlName); if (mi >= 0) { master[mi].paid = (master[mi].paid || 0) + net; master[mi].balance = (master[mi].contract_sum || master[mi].value || 0) - master[mi].paid; master[mi].lastPaymentDate = date; const certEntry = { cert_no: ref, date, period_from: periodFrom, period_to: periodTo, works: narration, gross, certified: amount, retention_pct: retPct, retention_amt: Math.round(amount*retPct/100), net, method, bank_ref: bankRef, certifying_engineer: certEng, recorded_by: by }; if (!master[mi].paid_history) master[mi].paid_history = []; master[mi].paid_history.unshift(certEntry); saveMasterContractors(master); syncMasterToLegacy(master); } // Also update legacy contractors store const ctrs = getData('contractors'); const ci = ctrs.findIndex(c => c.name === ctrlName); if (ci >= 0) { ctrs[ci].paid = (ctrs[ci].paid||0) + net; ctrs[ci].lastPaymentDate = date; save('contractors', ctrs); } // Log to contractorPayments const payments = getData('contractorPayments'); payments.unshift({contractor:ctrlName, date, amount:net, gross, method, ref, by, narration, periodFrom, periodTo, bankRef, certEng}); save('contractorPayments', payments); handleFileAttach('pay-attach', 'payments', date + '-' + ctrlName); addActivity('Payment: ' + fmtShort(net) + ' net to ' + ctrlName); closeModal('record-payment-modal'); renderRealContractorListNew(); renderPaymentSchedule(); renderPaymentTable(); } function generateCertFromPayModal() { // Generate cert from the pay modal data then open print const name = (document.getElementById('pay-contractor')||{}).value; const certified = +(document.getElementById('pay-amount')||{}).value || 0; const retPct = +(document.getElementById('pay-ret-pct')||{}).value || 5; const retAmt = +(document.getElementById('pay-ret-amt')||{}).value || Math.round(certified*retPct/100); const net = +(document.getElementById('pay-net')||{}).value || certified - retAmt; const certNo = (document.getElementById('pay-ref')||{}).value || 'PC-001'; const from = (document.getElementById('pay-period-from')||{}).value; const to = (document.getElementById('pay-period-to')||{}).value; const works = (document.getElementById('pay-narration')||{}).value; const eng = (document.getElementById('pay-certifying-eng')||{}).value; const gross = +(document.getElementById('pay-gross')||{}).value || certified; const master = getMasterContractors(); const c = master.find(x => x.name === name); _showCertPreview({name, certNo, from, to, certified, gross, retPct, retAmt, net, works, eng, c}); closeModal('record-payment-modal'); } function _showCertPreview({name, certNo, from, to, certified, gross, retPct, retAmt, net, works, eng, c}) { const contractSum = c ? (c.contract_sum||c.value||0) : 0; const cumPaid = c ? (c.paid||0) : 0; const balanceRemaining = contractSum - cumPaid - net; const worksHtml = works ? works.split('\n').filter(Boolean).map((l,i)=>`${i+1}${l.trim()}`).join('') : '—As per scope of works'; const header = rptHdr('PAYMENT CERTIFICATE No. ' + certNo, 'Infrastructure & Construction', 'Nedy Edrine Muziru — Infrastructure PM', certNo); const html = header + `
Contractor Name${name}
Contract No${c?c.contract_no||'—':'—'}
Contract Sum (UGX)${contractSum.toLocaleString()}
Period of Works${from||'—'} to ${to||'—'}
Site${c?c.site||'Coffee Park':'Coffee Park'}

WORKS COMPLETED THIS PERIOD

${worksHtml}
#Description of Works

FINANCIAL SUMMARY

Gross Amount Claimed by ContractorUGX ${gross.toLocaleString()}
Amount Certified by EngineerUGX ${certified.toLocaleString()}
Retention Deduction (${retPct}%)- UGX ${retAmt.toLocaleString()}
NET AMOUNT PAYABLE THIS CERTIFICATEUGX ${net.toLocaleString()}
Previously Certified & Paid (Cumulative)UGX ${cumPaid.toLocaleString()}
Balance Remaining on ContractUGX ${balanceRemaining.toLocaleString()}
Site Engineer / Clerk of Works
Tumuramye Julius
Quantity Surveyor
${eng||'Frank Odoi & Assoc.'}
Project Manager
Kubariho Venture
CEO / Authorizing Officer
Nelson Tugume

This certificate is issued subject to verification of works by the site supervision team. All amounts are in Uganda Shillings (UGX). Payment shall be processed through the Finance Department and requires dual authorization per IACP financial policy.

`; document.getElementById('report-preview-title').textContent = 'Payment Certificate — ' + certNo + ' — ' + name; document.getElementById('report-preview-body').innerHTML = html; openModal('report-preview-modal'); } // Override generateCert (from payment cert sub-panel) function generateCert() { const sel = document.getElementById('cert-contractor'); const id = sel ? parseInt(sel.value) : 0; const c_pay = REAL_PAYMENTS.find(r => r.n === id); const master = getMasterContractors(); const c = master.find(x => x.n === id) || (c_pay ? {name: c_pay.name, contract_sum: c_pay.contract, value: c_pay.contract, paid: c_pay.paid} : null); if (!c) { alert('Please select a contractor first.'); return; } const certified = +((document.getElementById('cert-amount')||{}).value) || 0; const retPct = +((document.getElementById('cert-retention')||{}).value) || 5; const retAmt = +((document.getElementById('cert-ret-amt')||{}).value) || Math.round(certified*retPct/100); const net = +((document.getElementById('cert-net')||{}).value) || (certified - retAmt); const certNo = (document.getElementById('cert-no')||{}).value || 'IACP/PC/2026/001'; const from = (document.getElementById('cert-from')||{}).value; const to = (document.getElementById('cert-to')||{}).value; const works = (document.getElementById('cert-works')||{}).value; const eng = (document.getElementById('cert-engineer')||{}).value; const gross = +((document.getElementById('cert-claimed')||{}).value) || certified; _showCertPreview({name: c.name, certNo, from, to, certified, gross, retPct, retAmt, net, works, eng, c}); } // ============================================================ // PRINT REPORTS // ============================================================ /** Payment Schedule Report — all 41 contractors */ function printPaymentScheduleReport() { const master = getMasterContractors(); const header = rptHdr('CONTRACTOR PAYMENT SCHEDULE', 'Infrastructure & Construction', 'Nedy Edrine Muziru — Infrastructure PM', 'IACP/PPL/2026/Q2'); const totContract = master.reduce((s,c)=>s+(c.contract_sum||c.value||0),0); const totPaid = master.reduce((s,c)=>s+(c.paid||0),0); const totBal = master.reduce((s,c)=>s+((c.contract_sum||c.value||0)-(c.paid||0)),0); const totProp = master.reduce((s,c)=>s+(c.proposed||0),0); const rows = master.map((c,i)=>{ const cs = c.contract_sum||c.value||0; const bal = cs-(c.paid||0); const pct = cs ? Math.round((c.paid||0)/cs*100) : 0; return ` ${c.n||i+1} ${c.name} ${c.discipline||c.spec||'—'} ${cs.toLocaleString()} ${(c.paid||0).toLocaleString()} ${bal.toLocaleString()} ${(c.requested||0).toLocaleString()} ${(c.proposed||0).toLocaleString()} ${pct}% ${c.status||'Active'} `; }).join(''); const html = header + `
SITE CONTRACTORS PAYMENT PLAN — As at ${new Date().toLocaleDateString('en-UG',{day:'2-digit',month:'long',year:'numeric'})} | Total Contractors: ${master.length}
Reference: IACP/PPL/2026/Q2 | Finance Dept: Hillary Obukui | PM: Kubariho Venture
${rows}
# Contractor Name Discipline Contract Sum (UGX) Paid (UGX) Balance (UGX) Requested (UGX) Proposed (UGX) Paid % Status
TOTALS (${master.length} Contractors) ${totContract.toLocaleString()} ${totPaid.toLocaleString()} ${totBal.toLocaleString()} ${totProp.toLocaleString()} ${totContract?Math.round(totPaid/totContract*100):0}%
Head of Contractors
Allan Nkabahita
Project Manager
Kubariho Venture
CEO & Authorizing Officer
Nelson Tugume
`; document.getElementById('report-preview-title').textContent = 'Contractor Payment Schedule'; document.getElementById('report-preview-body').innerHTML = html; openModal('report-preview-modal'); } /** Monthly Progress Report — printable */ function printMonthlyProgressReport() { const prData = getData('progress_main') || PROGRESS_MAIN; const header = rptHdr('MONTHLY PROGRESS REPORT — MAIN SITE', 'Infrastructure & Construction', 'Tumuramye Julius / Kyorishaba Favour', 'IACP/RPT/MAR/2026'); const totOngoing = prData.filter(r=>r.status==='Ongoing').length; const totHalted = prData.filter(r=>r.status==='Halted').length; const totInactive = prData.filter(r=>r.status==='Inactive'||r.status==='Pending').length; const rows = prData.map((r,i)=>` ${r.ref||i+1} ${r.contractor} ${r.section} ${r.pct||0}% ${r.status} ${r.achievement||'—'} ${r.remarks||''} `).join(''); const nyData = getData('progress_nyabihoko') || PROGRESS_NYABIHOKO; const nyRows = nyData.map((r,i)=>` ${r.ref||i+1} ${r.contractor} ${r.section} ${r.pct||0}% ${r.status} ${r.remarks||''} `).join(''); const html = header + `
Reporting Period: March 2026 | Report Date: ${new Date().toLocaleDateString('en-UG',{day:'2-digit',month:'long',year:'numeric'})}
Prepared by: Tumuramye Julius / Kyorishaba Favour | Revised by: Allan Nkabahita
${totOngoing}
Ongoing Works
${totHalted}
Halted Works
${totInactive}
Inactive / Pending

A. MAIN SITE — COFFEE PARK (${prData.length} Contractors)

${rows}
# Contractor Section of Work % Done Status Key Achievement Remarks

B. NYABIHOKO SITE (${nyData.length} Contractors)

${nyRows}
# Contractor Section of Work % Done Status Remarks
Clerk of Works
Tumuramye Julius
Site Supervisor
Allan Nkabahita
Project Manager
Kubariho Venture
`; document.getElementById('report-preview-title').textContent = 'Monthly Progress Report — March 2026'; document.getElementById('report-preview-body').innerHTML = html; openModal('report-preview-modal'); } function printNyabihokoProgressReport() { const nyData = getData('progress_nyabihoko') || PROGRESS_NYABIHOKO; const header = rptHdr('MONTHLY PROGRESS REPORT — NYABIHOKO', 'Infrastructure & Construction', 'Christine Blessing — Nyabihoko Site Supervisor', 'IACP/NY/RPT/MAR/2026'); const rows = nyData.map((r,i)=>` ${r.ref||i+1} ${r.contractor} ${r.section} ${r.pct||0}% ${r.status} ${r.remarks||''} `).join(''); const html = header + `
Reporting Period: March 2026 | Site: Nyabihoko, Ntungamo | Prepared by: Christine Blessing
${rows}
#Contractor Section% Done StatusRemarks
URGENT MATERIAL REQUESTS — NYABIHOKO: Hard Core (HIGH), Cement CEM IV (MEDIUM), Cement CEM II (MEDIUM), Sand & Aggregates (MEDIUM), Stone Dust (MEDIUM)
CONCLUSION: Labourers need more financial support to motivate them to be more active for better progress of work. Immediate attention should be given to urgent materials on site.
Site Supervisor — Nyabihoko
Christine Blessing
Project Manager
Kubariho Venture
`; document.getElementById('report-preview-title').textContent = 'Nyabihoko Progress Report — March 2026'; document.getElementById('report-preview-body').innerHTML = html; openModal('report-preview-modal'); } /** Site Daily Log Report */ function printSiteLogReport() { const sl = getData('sitelog'); const header = rptHdr('SITE DAILY LOG REPORT', 'Infrastructure & Construction', 'Tumuramye Julius — Clerk of Works', 'IACP/LOG/2026'); const rows = sl.map((e,i)=>` ${e.date} ${e.zone} ${e.contractor} ${e.by} ${e.weather||'—'} ${e.labor||0} ${e.progress||0}% ${(e.workdone||'').slice(0,80)+(e.workdone&&e.workdone.length>80?'...':'')} ${(e.issues||'').slice(0,60)+(e.issues&&e.issues.length>60?'...':'')} `).join(''); const html = header + `
SITE LOG DIARY — Generated: ${new Date().toLocaleDateString('en-UG',{day:'2-digit',month:'long',year:'numeric'})} | Entries: ${sl.length}
Location: Inspire Africa Coffee Park, Rwashameire, Ntungamo District
${rows.length ? rows : ''}
DateZoneContractor ByWeatherLabour ProgressWork DoneIssues
No site log entries recorded.
Clerk of Works
Tumuramye Julius
Head of Contractors
Allan Nkabahita
`; document.getElementById('report-preview-title').textContent = 'Site Daily Log Report'; document.getElementById('report-preview-body').innerHTML = html; openModal('report-preview-modal'); } /** Snag / Defects Report */ function printSnagReport() { const snags = getData('snags'); const open = snags.filter(s => s.status !== 'Closed'); const header = rptHdr('SNAG LIST / DEFECTS REPORT', 'Infrastructure & Construction', 'Site Engineer — Quality & Defects', 'IACP/SNR/2026'); const crit = open.filter(s=>s.severity==='Critical').length; const maj = open.filter(s=>s.severity==='Major').length; const min = open.filter(s=>s.severity==='Minor').length; const rows = open.map((s,i)=>{ const sevCol = s.severity==='Critical'?'#c0392b':s.severity==='Major'?'#d4621a':'#2a7a7a'; return ` ${s.num} ${s.zone} ${s.desc} ${s.cat} ${s.severity} ${s.raisedBy} ${s.dateRaised} ${s.assignedTo} ${s.targetDate} ${s.status} `; }).join(''); const html = header + `
${crit}
Critical
${maj}
Major
${min}
Minor
${rows || ''}
Snag #ZoneDescription CategorySeverityRaised By Date RaisedAssigned To Target CloseStatus
No open snag items — all defects resolved.
NOTE: All Critical and Major snags must be resolved before practical completion certificate can be issued. Contractor sign-off required on each remediation.
Site Inspector
Kyorishaba Favour
Head of Contractors
Allan Nkabahita
Project Manager
Kubariho Venture
`; document.getElementById('report-preview-title').textContent = 'Snag List / Defects Report'; document.getElementById('report-preview-body').innerHTML = html; openModal('report-preview-modal'); } /** Contractor Work Verification Sheet */ function printContractorVerification(idx) { const master = getMasterContractors(); const c = master[idx]; if (!c) return; const boq = getData('boq').filter(b => { // match zone if contractor's zones are recorded or by name return true; // include all for now }).slice(0, 15); const header = rptHdr('CONTRACTOR WORK VERIFICATION SHEET', 'Infrastructure & Construction', 'Site Engineer — Kyorishaba Favour', 'IACP/CVS/' + (c.n||idx+1).toString().padStart(3,'0')); const boqRows = boq.length > 0 ? boq.map((b,i)=>` ${b.itemNo||'—'} ${b.desc||'—'} ${(b.qty||0).toLocaleString()} ${b.unit||'—'} ${(b.unitRate||0).toLocaleString()} ${((b.qty||0)*(b.unitRate||0)).toLocaleString()} ${b.pctComplete||0}%                 `).join('') : `BOQ items will appear here when linked to this contractor's zones`; const html = header + `
Contractor Name${c.name}Contract No${c.contract_no||'—'}
Discipline${c.discipline||c.spec||'—'}Site${c.site||'Coffee Park'}
Contract Sum (UGX)${(c.contract_sum||c.value||0).toLocaleString()}Paid to Date${(c.paid||0).toLocaleString()}
Inspection Date${today()}Supervising Engineer${c.engineer||'Kyorishaba Favour'}

WORKS VERIFICATION — BILL OF QUANTITIES

${boqRows}
Item NoDescription of Works QtyUnit Rate (UGX)Amount (UGX) % VerifiedField Sign-Off
SITE SUPERVISOR VERIFICATION REMARKS:
Contractor Representative
Name: ___________________
Date: ___________________
Site Engineer / Supervisor
${c.engineer||'Kyorishaba Favour'}
Date: ___________________
Head of Contractors
Allan Nkabahita
Date: ___________________
`; document.getElementById('report-preview-title').textContent = 'Work Verification Sheet — ' + c.name; document.getElementById('report-preview-body').innerHTML = html; openModal('report-preview-modal'); } // ============================================================ // PROGRESS REPORT EDIT (editable PROGRESS_MAIN / PROGRESS_NYABIHOKO) // ============================================================ // Mutable copies stored in localStorage function getProgressMain() { try { const d=JSON.parse(localStorage.getItem('progress_main')); if(d&&d.length)return d; } catch(e){} const copy = JSON.parse(JSON.stringify(PROGRESS_MAIN)); localStorage.setItem('progress_main', JSON.stringify(copy)); return copy; } function saveProgressMain(d) { localStorage.setItem('progress_main', JSON.stringify(d)); syncProgressToApi(d); // fire-and-forget sync to PostgreSQL } function getProgressNyabihoko() { try { const d=JSON.parse(localStorage.getItem('progress_nyabihoko')); if(d&&d.length)return d; } catch(e){} const copy = JSON.parse(JSON.stringify(PROGRESS_NYABIHOKO)); localStorage.setItem('progress_nyabihoko', JSON.stringify(copy)); return copy; } function saveProgressNyabihoko(d) { localStorage.setItem('progress_nyabihoko', JSON.stringify(d)); syncProgressToApi(d); // fire-and-forget sync to PostgreSQL } function openProgressEntryModal(site, idx) { const modal = document.getElementById('progress-entry-modal'); if (!modal) return; const titleEl = document.getElementById('pr-entry-title'); document.getElementById('pr-entry-site').value = site; document.getElementById('pr-entry-idx').value = idx; // Populate contractor dropdown from master const master = getMasterContractors(); const ctrlSel = document.getElementById('pr-entry-contractor'); if (ctrlSel) { ctrlSel.innerHTML = '' + master.map(c=>``).join(''); } // Populate building dropdown from BOQ const bldgSel = document.getElementById('pr-entry-building'); if (bldgSel) { const boqZones = [...new Set(getData('boq').map(b=>b.zone).filter(Boolean))]; const cpBuildings = BOQ_COFFEE_PARK.map(b=>b.name); const nyBuildings = BOQ_NYABIHOKO.map(b=>b.name); const allBldgs = [...new Set([...boqZones, ...cpBuildings, ...nyBuildings])]; bldgSel.innerHTML = '' + allBldgs.map(b=>``).join(''); } if (idx < 0) { if (titleEl) titleEl.textContent = 'Add Progress Entry — ' + (site==='main'?'Main Site':'Nyabihoko'); ['pr-entry-section','pr-entry-achievement','pr-entry-challenges','pr-entry-nextplan','pr-entry-remarks'].forEach(id=>{const el=document.getElementById(id);if(el)el.value='';}); const ws = document.getElementById('pr-entry-workers'); if(ws) ws.value = 0; const pt = document.getElementById('pr-entry-pct'); if(pt) pt.value = 0; } else { if (titleEl) titleEl.textContent = 'Edit Progress Entry'; const data = site === 'main' ? getProgressMain() : getProgressNyabihoko(); const r = data[idx]; if (!r) return; const set = (id,val) => { const el=document.getElementById(id); if(el) el.value=val||''; }; // Set contractor if (ctrlSel) { for (let o of ctrlSel.options) if (o.text === r.contractor) { o.selected=true; break; } } set('pr-entry-section', r.section); const ptEl = document.getElementById('pr-entry-pct'); if(ptEl) ptEl.value = r.pct||0; const wsEl = document.getElementById('pr-entry-workers'); if(wsEl) wsEl.value = r.workers||0; set('pr-entry-status', r.status); set('pr-entry-achievement', r.achievement); set('pr-entry-challenges', r.challenges||''); set('pr-entry-nextplan', r.nextplan||''); set('pr-entry-remarks', r.remarks); } modal.classList.remove('hidden'); } function saveProgressEntry() { const site = document.getElementById('pr-entry-site').value; const idx = parseInt(document.getElementById('pr-entry-idx').value); const contractor = (document.getElementById('pr-entry-contractor')||{}).value||''; const building = (document.getElementById('pr-entry-building')||{}).value||''; const section = (document.getElementById('pr-entry-section')||{}).value.trim(); const pct = parseInt((document.getElementById('pr-entry-pct')||{}).value)||0; const workers = parseInt((document.getElementById('pr-entry-workers')||{}).value)||0; const status = (document.getElementById('pr-entry-status')||{}).value||'Ongoing'; const achievement = (document.getElementById('pr-entry-achievement')||{}).value||''; const challenges = (document.getElementById('pr-entry-challenges')||{}).value||''; const nextplan = (document.getElementById('pr-entry-nextplan')||{}).value||''; const remarks = (document.getElementById('pr-entry-remarks')||{}).value||''; if (!contractor || !section) return alert('Contractor and Section of Work are required.'); const data = site === 'main' ? getProgressMain() : getProgressNyabihoko(); const entry = { ref: idx < 0 ? String(data.length+1) : (data[idx]||{}).ref||String(idx+1), contractor, section: building?(building+' — '+section):section, pct, workers, status, achievement, challenges, nextplan, remarks }; if (idx < 0) { data.push(entry); } else { data[idx] = { ...data[idx], ...entry }; } if (site === 'main') saveProgressMain(data); else saveProgressNyabihoko(data); addActivity('Progress entry ' + (idx<0?'added':'updated') + ': ' + contractor + ' — ' + section); closeModal('progress-entry-modal'); renderProgressReport(); } // ============================================================ // BOQ ITEM EDITING // ============================================================ function openBoqItemModal(idx) { const modal = document.getElementById('boq-item-modal'); if (!modal) return; document.getElementById('boq-item-idx').value = idx < 0 ? '' : idx; document.getElementById('boq-item-title').textContent = idx < 0 ? 'Add BOQ Item' : 'Edit BOQ Item'; // Populate zone dropdown const zones = [...new Set([...getData('boq').map(b=>b.zone), ...BOQ_COFFEE_PARK.map(b=>b.name), ...BOQ_NYABIHOKO.map(b=>'Nyabihoko — '+b.name)])].filter(Boolean); const zSel = document.getElementById('boq-item-zone'); if (zSel) zSel.innerHTML = '' + zones.map(z=>``).join(''); if (idx >= 0) { const boq = getData('boq')[idx]; if (!boq) return; const set = (id,val) => { const el=document.getElementById(id); if(el) el.value=val||''; }; set('boq-item-no', boq.itemNo); if (zSel) { for(let o of zSel.options) if(o.text===boq.zone){o.selected=true;break;} } set('boq-item-desc', boq.desc); set('boq-item-qty', boq.qty||0); const uSel = document.getElementById('boq-item-unit'); if(uSel) uSel.value = boq.unit||'m³'; set('boq-item-rate', boq.unitRate||0); set('boq-item-amount', (boq.qty||0)*(boq.unitRate||0)); set('boq-item-pct', boq.pctComplete||0); set('boq-item-actual', boq.actualCost||0); set('boq-item-notes', boq.notes||''); } else { ['boq-item-no','boq-item-desc','boq-item-qty','boq-item-rate','boq-item-amount','boq-item-pct','boq-item-actual','boq-item-notes'].forEach(id=>{const el=document.getElementById(id);if(el)el.value='';}); } // Auto-calc amount on qty/rate change ['boq-item-qty','boq-item-rate'].forEach(id => { const el = document.getElementById(id); if (el) el.oninput = () => { const qty = parseFloat(document.getElementById('boq-item-qty').value)||0; const rate = parseFloat(document.getElementById('boq-item-rate').value)||0; const amtEl = document.getElementById('boq-item-amount'); if(amtEl) amtEl.value = Math.round(qty*rate); }; }); modal.classList.remove('hidden'); } function saveBoqItem() { const desc = (document.getElementById('boq-item-desc')||{}).value.trim(); if (!desc) return alert('Description is required.'); const idx = document.getElementById('boq-item-idx').value; const boq = getData('boq'); const qty = parseFloat((document.getElementById('boq-item-qty')||{}).value)||0; const rate = parseFloat((document.getElementById('boq-item-rate')||{}).value)||0; const entry = { itemNo: (document.getElementById('boq-item-no')||{}).value||'Z-'+String(boq.length+1).padStart(3,'0'), zone: (document.getElementById('boq-item-zone')||{}).value||'', desc, unit: (document.getElementById('boq-item-unit')||{}).value||'m³', qty, unitRate: rate, pctComplete: parseFloat((document.getElementById('boq-item-pct')||{}).value)||0, actualCost: parseFloat((document.getElementById('boq-item-actual')||{}).value)||0, notes: (document.getElementById('boq-item-notes')||{}).value||'' }; if (idx === '') { boq.push(entry); addActivity('BOQ item added: '+desc); } else { boq[parseInt(idx)] = entry; addActivity('BOQ item updated: '+desc); } save('boq', boq); closeModal('boq-item-modal'); } // ============================================================ // ZONE EDITING // ============================================================ async function openZoneEditModal(zoneId) { const zones = getData('zones'); const z = zones.find(x => x.id === zoneId); if (!z) return; const master = getMasterContractors(); const ctrlSel = document.getElementById('ez-contractor'); if (ctrlSel) ctrlSel.innerHTML = master.map(c=>``).join(''); document.getElementById('ez-id').value = zoneId; const set = (id,val)=>{ const el=document.getElementById(id); if(el) el.value=val||''; }; set('ez-name', z.name); if (ctrlSel) { for(let o of ctrlSel.options) if(o.text===z.contractor){o.selected=true;break;} } set('ez-progress', z.progress||0); set('ez-status', z.status||'In Progress'); set('ez-budget', z.budget||0); set('ez-start', z.startDate||''); set('ez-target', z.targetDate||''); set('ez-description', z.description||''); document.getElementById('edit-zone-modal').classList.remove('hidden'); } function saveZoneEdit() { const id = parseInt(document.getElementById('ez-id').value); const zones = getData('zones'); const i = zones.findIndex(z => z.id === id); if (i < 0) return; zones[i] = { ...zones[i], name: (document.getElementById('ez-name')||{}).value.trim() || zones[i].name, contractor: (document.getElementById('ez-contractor')||{}).value || zones[i].contractor, progress: parseInt((document.getElementById('ez-progress')||{}).value)||zones[i].progress, status: (document.getElementById('ez-status')||{}).value || zones[i].status, budget: parseFloat((document.getElementById('ez-budget')||{}).value)||zones[i].budget, startDate: (document.getElementById('ez-start')||{}).value||zones[i].startDate, targetDate: (document.getElementById('ez-target')||{}).value||zones[i].targetDate, description: (document.getElementById('ez-description')||{}).value||zones[i].description }; save('zones', zones); addActivity('Zone ' + id + ' updated: ' + zones[i].name); closeModal('edit-zone-modal'); renderZoneList(); renderOverview(); } // ============================================================ // EQUIPMENT FORM (unified add/edit) // ============================================================ function openEquipmentForm(idx) { const modal = document.getElementById('add-equipment-modal'); if (!modal) return; document.getElementById('eq-edit-idx').value = idx < 0 ? '' : idx; document.getElementById('eq-modal-title').textContent = idx < 0 ? 'Add Equipment' : 'Edit Equipment'; if (idx >= 0) { const eq = getData('equipment')[idx]; if (!eq) return; const set = (id,val)=>{ const el=document.getElementById(id); if(el) el.value=val||''; }; set('eq-id', eq.id); set('eq-name', eq.name); set('eq-cat', eq.cat||'Civil'); set('eq-zone', eq.zone||''); set('eq-operator', eq.operator||''); set('eq-status', eq.status||'Operational'); set('eq-cond', eq.cond||'Good'); set('eq-fuel', eq.fuel||'Diesel'); set('eq-date-in', eq.dateIn||''); set('eq-purchase-date', eq.purchaseDate||''); set('eq-purchase-value', eq.purchaseValue||0); set('eq-service', eq.service||''); set('eq-next-service', eq.nextServiceDate||''); set('eq-notes', eq.notes||''); } else { ['eq-id','eq-name','eq-zone','eq-operator','eq-date-in','eq-purchase-date','eq-notes'].forEach(id=>{const el=document.getElementById(id);if(el)el.value='';}); const pvEl=document.getElementById('eq-purchase-value');if(pvEl)pvEl.value=0; } modal.classList.remove('hidden'); } function saveEquipmentForm() { const id = (document.getElementById('eq-id')||{}).value.trim(); const name = (document.getElementById('eq-name')||{}).value.trim(); if (!id || !name) return alert('Asset ID and name are required.'); const eq = getData('equipment'); const idxStr = document.getElementById('eq-edit-idx').value; const entry = { id, name, cat: (document.getElementById('eq-cat')||{}).value||'Civil', zone: (document.getElementById('eq-zone')||{}).value||'', operator: (document.getElementById('eq-operator')||{}).value||'', status: (document.getElementById('eq-status')||{}).value||'Operational', cond: (document.getElementById('eq-cond')||{}).value||'Good', fuel: (document.getElementById('eq-fuel')||{}).value||'Diesel', dateIn: (document.getElementById('eq-date-in')||{}).value||'', purchaseDate: (document.getElementById('eq-purchase-date')||{}).value||'', purchaseValue: +((document.getElementById('eq-purchase-value')||{}).value)||0, service: (document.getElementById('eq-service')||{}).value||'', nextServiceDate: (document.getElementById('eq-next-service')||{}).value||'', notes: (document.getElementById('eq-notes')||{}).value||'' }; if (idxStr === '') { eq.push(entry); addActivity('Equipment added: ' + name + ' (' + id + ')'); } else { eq[parseInt(idxStr)] = { ...(eq[parseInt(idxStr)]||{}), ...entry }; addActivity('Equipment updated: ' + name); } save('equipment', eq); populateEqFilters(); renderEquipmentList(); renderServiceDue(); closeModal('add-equipment-modal'); } // Override saveEquipment to use unified form function saveEquipment() { saveEquipmentForm(); } // Open equipment edit function editEquipment(idx) { openEquipmentForm(idx); } // ============================================================ // UPDATE RENDERERS TO USE MASTER DATA & ADD EDIT BUTTONS // ============================================================ // Patch renderPaymentTable to include action buttons const _origRenderPaymentTable = renderPaymentTable; function renderPaymentTable() { const tbody = document.getElementById('payments-tbody'); const tfoot = document.getElementById('payments-tfoot'); if (!tbody) return; const master = getMasterContractors(); const search = ((document.getElementById('pay-search')||{}).value||'').toLowerCase(); const rows = master.filter(r => !search || (r.name||'').toLowerCase().includes(search)); tbody.innerHTML = rows.map((r, i) => { const cs = r.contract_sum || r.value || 0; const paid = r.paid || 0; const bal = cs - paid; const paidPct = cs ? Math.round(paid/cs*100) : 0; const masterIdx = master.indexOf(r); return ` ${r.n||i+1} ${r.name} ${cs.toLocaleString()} ${paid.toLocaleString()}
${bal.toLocaleString()} ${(r.requested||0).toLocaleString()} ${(r.proposed||0).toLocaleString()} Pending CEO `; }).join(''); const totContract = master.reduce((s,r)=>s+(r.contract_sum||r.value||0),0); const totPaid = master.reduce((s,r)=>s+(r.paid||0),0); const totBal = master.reduce((s,r)=>s+((r.contract_sum||r.value||0)-(r.paid||0)),0); const totReq = master.reduce((s,r)=>s+(r.requested||0),0); const totProp = master.reduce((s,r)=>s+(r.proposed||0),0); if (tfoot) tfoot.innerHTML = `TOTALS (${master.length} Contractors)${totContract.toLocaleString()}${totPaid.toLocaleString()}${totBal.toLocaleString()}${totReq.toLocaleString()}${totProp.toLocaleString()}`; } // renderProgressReport — canonical version defined above at line ~4865 (now uses dynamic data + edit buttons) // Patch renderEquipmentList to add edit button const _origRenderEquipmentList = renderEquipmentList; function renderEquipmentList() { let eq = getData('equipment'); const search = ((document.getElementById('eq-search')||{}).value||'').toLowerCase(); const catF = (document.getElementById('eq-cat-filter')||{}).value||''; const stF = (document.getElementById('eq-status-filter')||{}).value||''; const zF = (document.getElementById('eq-zone-filter')||{}).value||''; eq = eq.filter(e=>{ if(search&&!(e.name||'').toLowerCase().includes(search)&&!(e.id||'').toLowerCase().includes(search))return false; if(catF&&e.cat!==catF)return false; if(stF&&e.status!==stF)return false; if(zF&&e.zone!==zF)return false; return true; }); const sMap={'Operational':'badge-green','Installed':'badge-blue','Standby':'badge-amber','Maintenance':'badge-red','Commissioning':'badge-teal','Decommissioned':'badge-grey'}; const cMap={'Excellent':'badge-green','Good':'badge-teal','Fair':'badge-amber','Poor':'badge-red'}; const allEq = getData('equipment'); const tbodyEl = document.getElementById('equipment-tbody'); if (!tbodyEl) return; tbodyEl.innerHTML = eq.map(e => { const days = daysFrom(e.nextServiceDate); const dueCls = days!==null&&days<=0?'color:var(--red);font-weight:700':days!==null&&days<=14?'color:var(--orange);font-weight:700':''; const ri = allEq.findIndex(x=>x.id===e.id); return ` ${e.id}${e.name}
${e.operator||''} ${e.cat}${e.zone} ${e.status} ${e.cond} ${e.purchaseDate||'—'} ${e.purchaseValue?fmtShort(e.purchaseValue):'—'} ${e.service||'—'} ${e.nextServiceDate||'—'}${days!==null&&days<=0?' (OVERDUE)':days!==null&&days<=14?' ('+days+'d)':''} ${renderAttachBadge('equipment',e.id)} `; }).join(''); } // ============================================================ // POPULATE cert-contractor from master + fix openModal for pay // ============================================================ function populateCertContractorDropdown() { const sel = document.getElementById('cert-contractor'); if (!sel) return; const master = getMasterContractors(); sel.innerHTML = '' + master.map(r=>``).join(''); } // openModal — now defined in the new functions block below (handles all modals including new ones) // ============================================================ // INIT // ============================================================ document.title='Infrastructure & Construction — Africa Coffee Park ERP'; (function init(){ // Force refresh zones seed if stored count < 34 (old 18-zone data) try{ const stored=JSON.parse(localStorage.getItem('erp_zones')||'[]'); if(!stored||stored.length<34){localStorage.removeItem('erp_zones');} }catch(e){} // Also clear stale sitelog/snag seeds that reference old "Zone N" format try{ const sl=JSON.parse(localStorage.getItem('erp_sitelog')||'[]'); if(sl.some&&sl.some(e=>e.zone&&/^Zone \d+ —/.test(e.zone))){localStorage.removeItem('erp_sitelog');} }catch(e){} try{ const sn=JSON.parse(localStorage.getItem('erp_snags')||'[]'); if(sn.some&&sn.some(e=>e.zone&&/^Zone \d+ —/.test(e.zone))){localStorage.removeItem('erp_snags');} }catch(e){} ['zones','contractors','equipment','sitelog','snags','boq','variations','documents','paymentMilestones','contractorPayments','equipmentServiceLog','attachments'].forEach(k=>getData(k)); // Bootstrap master contractor list from REAL_PAYMENTS on first load getMasterContractors(); renderOverview(); })(); // How to Use — handled as static panel; no render needed // ============================================================ // MODAL RESET HELPERS — populate contractor dropdowns & defaults // ============================================================ function resetChallengeModal() { const c = getMasterContractors(); const sel = document.getElementById('ch-contractor'); if (sel) sel.innerHTML = '' + c.map(r=>``).join(''); const d = document.getElementById('ch-date'); if(d) d.value = today(); const imp = document.getElementById('ch-impact'); if(imp) imp.value = 'High'; const cat = document.getElementById('ch-category'); if(cat) cat.value = 'Logistics'; const st = document.getElementById('ch-status'); if(st) st.value = 'Open'; const t = document.getElementById('ch-title'); if(t) t.value = ''; const dc = document.getElementById('ch-desc'); if(dc) dc.value = ''; const ac = document.getElementById('ch-action'); if(ac) ac.value = ''; const id = document.getElementById('ch-edit-idx'); if(id) id.value = ''; } function resetStaffLogModal() { const c = getMasterContractors(); const sel = document.getElementById('sl-staff-contractor'); if (sel) sel.innerHTML = '' + c.map(r=>``).join(''); const d = document.getElementById('sl-staff-date'); if(d) d.value = today(); const r = document.getElementById('sl-staff-role'); if(r) r.value = ''; const cnt = document.getElementById('sl-staff-count'); if(cnt) cnt.value = ''; const z = document.getElementById('sl-staff-zone'); if(z) z.value = ''; const sup = document.getElementById('sl-staff-supervisor'); if(sup) sup.value = ''; const rem = document.getElementById('sl-staff-remarks'); if(rem) rem.value = ''; } function resetToolboxModal() { const c = getMasterContractors(); const sel = document.getElementById('tb-contractor'); if (sel) sel.innerHTML = '' + c.map(r=>``).join(''); const d = document.getElementById('tb-date'); if(d) d.value = today(); const by = document.getElementById('tb-conducted-by'); if(by) by.value = ''; const top = document.getElementById('tb-topic'); if(top) top.value = 'Safety'; const att = document.getElementById('tb-attendees'); if(att) att.value = ''; const z = document.getElementById('tb-zone'); if(z) z.value = ''; const nx = document.getElementById('tb-next-date'); if(nx) nx.value = ''; const st = document.getElementById('tb-status'); if(st) st.value = 'Open'; const kp = document.getElementById('tb-key-points'); if(kp) kp.value = ''; const ac = document.getElementById('tb-actions'); if(ac) ac.value = ''; const id = document.getElementById('tb-edit-idx'); if(id) id.value = ''; } // ============================================================ // SAVE FUNCTIONS — challenges, staff log, toolbox // ============================================================ function saveChallenge() { const records = JSON.parse(localStorage.getItem('erp_progress_challenges')||'[]'); const editIdx = document.getElementById('ch-edit-idx').value; const rec = { id: editIdx || Date.now().toString(), date: document.getElementById('ch-date').value, impact: document.getElementById('ch-impact').value, category: document.getElementById('ch-category').value, contractor: document.getElementById('ch-contractor').value, status: document.getElementById('ch-status').value, title: (document.getElementById('ch-title').value||'').trim(), desc: (document.getElementById('ch-desc').value||'').trim(), action: (document.getElementById('ch-action').value||'').trim() }; if (!rec.title) { alert('Please enter a challenge title.'); return; } if (editIdx) { const idx = records.findIndex(r => r.id === editIdx); if (idx > -1) records[idx] = rec; else records.push(rec); } else { records.push(rec); } localStorage.setItem('erp_progress_challenges', JSON.stringify(records)); closeModal('challenge-modal'); renderProgressChallengesEditable(); } function saveStaffLog() { const records = JSON.parse(localStorage.getItem('erp_progress_staff_log')||'[]'); const count = parseInt(document.getElementById('sl-staff-count').value) || 0; const rec = { id: Date.now().toString(), date: document.getElementById('sl-staff-date').value, contractor: document.getElementById('sl-staff-contractor').value, role: (document.getElementById('sl-staff-role').value||'').trim(), count: count, zone: (document.getElementById('sl-staff-zone').value||'').trim(), supervisor: (document.getElementById('sl-staff-supervisor').value||'').trim(), remarks: (document.getElementById('sl-staff-remarks').value||'').trim() }; if (!rec.contractor) { alert('Please select a contractor.'); return; } if (!rec.count) { alert('Please enter worker count.'); return; } records.push(rec); localStorage.setItem('erp_progress_staff_log', JSON.stringify(records)); closeModal('staff-log-modal'); renderSiteStaffLog(); } function saveToolboxObservation() { const records = JSON.parse(localStorage.getItem('erp_progress_toolbox')||'[]'); const editIdx = document.getElementById('tb-edit-idx').value; const rec = { id: editIdx || Date.now().toString(), date: document.getElementById('tb-date').value, by: (document.getElementById('tb-conducted-by').value||'').trim(), topic: document.getElementById('tb-topic').value, attendees: parseInt(document.getElementById('tb-attendees').value) || 0, contractor: document.getElementById('tb-contractor').value, zone: (document.getElementById('tb-zone').value||'').trim(), next: document.getElementById('tb-next-date').value, status: document.getElementById('tb-status').value, points: (document.getElementById('tb-key-points').value||'').trim(), actions: (document.getElementById('tb-actions').value||'').trim() }; if (editIdx) { const idx = records.findIndex(r => r.id === editIdx); if (idx > -1) records[idx] = rec; else records.push(rec); } else { records.push(rec); } localStorage.setItem('erp_progress_toolbox', JSON.stringify(records)); closeModal('toolbox-modal'); renderToolboxObservations(); } // ============================================================ // RENDER — Key Challenges (editable) // ============================================================ function renderProgressChallengesEditable() { const records = JSON.parse(localStorage.getItem('erp_progress_challenges')||'[]'); const el = document.getElementById('challenges-list'); if (!el) return; if (!records.length) { el.innerHTML = '

No challenges recorded. Click "+ Add Challenge" to log one.

'; return; } // Sort: open/in-progress first, then resolved; within each group, newest first const sorted = [...records].sort((a,b) => { const order = {Open:0,'In Progress':1,Resolved:2}; if (order[a.status] !== order[b.status]) return order[a.status] - order[b.status]; return b.date.localeCompare(a.date); }); el.innerHTML = sorted.map(r => { const impCls = r.impact === 'High' ? 'impact-high' : r.impact === 'Medium' ? 'impact-medium' : 'impact-low'; const resCls = r.status === 'Resolved' ? ' resolved' : ''; const impBadge = `${r.impact}`; const stBadge = `${r.status}`; return `
${impBadge} ${stBadge} ${r.category} ${r.date}${r.contractor ? ' · ' + r.contractor : ''}
${r.title}
Description: ${r.desc || '—'}
Recommended Action: ${r.action || '—'}
${r.status !== 'Resolved' ? `` : ''}
`; }).join(''); } function editChallenge(id) { const records = JSON.parse(localStorage.getItem('erp_progress_challenges')||'[]'); const r = records.find(x => x.id === id); if (!r) return; resetChallengeModal(); setTimeout(() => { document.getElementById('ch-edit-idx').value = r.id; document.getElementById('ch-date').value = r.date || today(); document.getElementById('ch-impact').value = r.impact || 'High'; document.getElementById('ch-category').value = r.category || 'Logistics'; document.getElementById('ch-contractor').value = r.contractor || ''; document.getElementById('ch-status').value = r.status || 'Open'; document.getElementById('ch-title').value = r.title || ''; document.getElementById('ch-desc').value = r.desc || ''; document.getElementById('ch-action').value = r.action || ''; }, 50); openModal('challenge-modal'); } function resolveChallenge(id) { const records = JSON.parse(localStorage.getItem('erp_progress_challenges')||'[]'); const idx = records.findIndex(r => r.id === id); if (idx > -1) { records[idx].status = 'Resolved'; localStorage.setItem('erp_progress_challenges', JSON.stringify(records)); } renderProgressChallengesEditable(); } function deleteChallenge(id) { if (!confirm('Delete this challenge record?')) return; const records = JSON.parse(localStorage.getItem('erp_progress_challenges')||'[]').filter(r => r.id !== id); localStorage.setItem('erp_progress_challenges', JSON.stringify(records)); renderProgressChallengesEditable(); } // ============================================================ // RENDER — Site Staff Log // ============================================================ function renderSiteStaffLog() { const records = JSON.parse(localStorage.getItem('erp_progress_staff_log')||'[]'); const el = document.getElementById('staff-log-list'); if (!el) return; if (!records.length) { el.innerHTML = '

No staff records. Click "+ Record Staff" to log daily attendance.

'; return; } const sorted = [...records].sort((a,b) => b.date.localeCompare(a.date)); const total = records.reduce((s,r) => s + (r.count||0), 0); const today_date = today(); const todayCount = records.filter(r => r.date === today_date).reduce((s,r) => s + (r.count||0), 0); el.innerHTML = `
${records.length}
Log Entries
${todayCount}
Workers Today
${total}
Total Worker-Days
${sorted.map(r => ``).join('')}
DateContractorRole / TradeWorkersZoneSupervisorRemarksActions
${r.date} ${r.contractor||'—'} ${r.role||'—'} ${r.count} ${r.zone||'—'} ${r.supervisor||'—'} ${r.remarks||'—'}
`; } function editStaffLog(id) { // Staff log entries don't support editing (append-only); redirect to delete + re-add if (!confirm('Staff logs are append-only. Delete this entry to re-enter?')) return; deleteStaffLog(id); } function deleteStaffLog(id) { if (!confirm('Delete this staff record?')) return; const records = JSON.parse(localStorage.getItem('erp_progress_staff_log')||'[]').filter(r => r.id !== id); localStorage.setItem('erp_progress_staff_log', JSON.stringify(records)); renderSiteStaffLog(); } // ============================================================ // RENDER — Toolbox Meeting Observations // ============================================================ function renderToolboxObservations() { const records = JSON.parse(localStorage.getItem('erp_progress_toolbox')||'[]'); const el = document.getElementById('toolbox-list'); if (!el) return; if (!records.length) { el.innerHTML = '

No toolbox meetings recorded. Click "+ Add Observation" to log one.

'; return; } const sorted = [...records].sort((a,b) => b.date.localeCompare(a.date)); el.innerHTML = `
${sorted.map(r => ``).join('')}
DateTopicConducted ByContractorZoneAttendeesStatusNext MeetingActions
${r.date} ${r.topic} ${r.by||'—'} ${r.contractor||'—'} ${r.zone||'—'} ${r.attendees||0} ${r.status} ${r.next||'—'}
`; } function editToolbox(id) { const records = JSON.parse(localStorage.getItem('erp_progress_toolbox')||'[]'); const r = records.find(x => x.id === id); if (!r) return; resetToolboxModal(); setTimeout(() => { document.getElementById('tb-edit-idx').value = r.id; document.getElementById('tb-date').value = r.date || today(); document.getElementById('tb-conducted-by').value = r.by || ''; document.getElementById('tb-topic').value = r.topic || 'Safety'; document.getElementById('tb-attendees').value = r.attendees || ''; document.getElementById('tb-contractor').value = r.contractor || ''; document.getElementById('tb-zone').value = r.zone || ''; document.getElementById('tb-next-date').value = r.next || ''; document.getElementById('tb-status').value = r.status || 'Open'; document.getElementById('tb-key-points').value = r.points || ''; document.getElementById('tb-actions').value = r.actions || ''; }, 50); openModal('toolbox-modal'); } function deleteToolbox(id) { if (!confirm('Delete this toolbox observation?')) return; const records = JSON.parse(localStorage.getItem('erp_progress_toolbox')||'[]').filter(r => r.id !== id); localStorage.setItem('erp_progress_toolbox', JSON.stringify(records)); renderToolboxObservations(); } // (duplicate openModal removed — canonical async version is defined above at line ~2413) // renderProgressReport — canonical version defined above (uses dynamic data + edit buttons) // ============================================================ // CONTRACTOR PERFORMANCE SCORE // ============================================================ function calcContractorPerfScore(c) { // c = contractor object from REAL_PAYMENTS / getMasterContractors() const snags = getData('snags', []); const siteLog = getData('sitelog', []); const payments = getData('contractorPayments', []); // 1. Progress vs Payment ratio (30 pts) // Use progress % from BOQ or zone data; approximate from REAL_PAYMENTS milestones const proposed = parseFloat(c.proposed)||0; const paid = parseFloat(c.paid)||0; const payRatio = proposed > 0 ? paid / proposed : 0; // Assume "fair" ratio if paid is proportional to reported progress (seed: progress in c or 50%) const progress = parseFloat(c.progress)||50; const progressRatio = progress / 100; let progressPayScore = 0; if (progressRatio > 0) { const ratio = Math.min(payRatio / progressRatio, 1.5); // how close payment is to progress progressPayScore = Math.min(30, Math.round(30 * (1 - Math.abs(1 - ratio) * 0.5))); } else { progressPayScore = paid === 0 ? 25 : 15; } // 2. Open Snags (20 pts) — deduct per open snag attributed to contractor const openSnags = snags.filter(s => (s.contractor||'').toLowerCase().includes((c.name||'').toLowerCase().substring(0,6)) && s.status !== 'Closed').length; const snagScore = Math.max(0, 20 - openSnags * 4); // 3. Payment Certificate Count (20 pts) — more certs = more active const payCerts = payments.filter(p => (p.contractor||'') === c.name).length; const certScore = Math.min(20, payCerts * 5); // 4. Site Attendance (15 pts) — staff log entries for this contractor const attendance = siteLog.filter(s => (s.contractor||'') === c.name).length; const attScore = Math.min(15, attendance * 3); // 5. Schedule Adherence (15 pts) — based on status field const status = (c.status||'Active').toLowerCase(); const schedScore = status.includes('complet') ? 15 : status.includes('active') ? 12 : status.includes('mobiliz') ? 8 : status.includes('suspend') ? 2 : 5; const total = progressPayScore + snagScore + certScore + attScore + schedScore; return { total: Math.min(100, total), progressPayScore, snagScore, certScore, attScore, schedScore }; } function perfStars(score) { const stars = Math.round(score / 20); // 0-5 stars return '★'.repeat(stars) + '☆'.repeat(5 - stars); } // ============================================================ // renderContractorsPerfTable — contractors list with performance scores // Called from contractors-stats panel and any panel with id "contractors-real-list" // ============================================================ function renderContractorsPerfTable() { const all = getMasterContractors(); const el = document.getElementById('contractors-real-list'); if (!el) return; const fv = (document.getElementById('ctr-filter-val')||{}).value||''; const fs = (document.getElementById('ctr-filter-status')||{}).value||''; let rows = all; if (fv) rows = rows.filter(r => r.name.toLowerCase().includes(fv.toLowerCase()) || (r.trade||'').toLowerCase().includes(fv.toLowerCase())); if (fs) rows = rows.filter(r => (r.status||'Active') === fs); const totalContract = rows.reduce((s,r)=>s+(parseFloat(r.proposed)||0),0); const totalPaid = rows.reduce((s,r)=>s+(parseFloat(r.paid)||0),0); el.innerHTML = `
${rows.length}
Contractors
UGX ${(totalContract/1e9).toFixed(2)}B
Total Contract Value
UGX ${(totalPaid/1e9).toFixed(2)}B
Total Paid
${totalContract>0?Math.round(totalPaid/totalContract*100):0}%
Overall Pay Rate
${rows.map((r,i) => { const proposed = parseFloat(r.proposed)||0; const paid = parseFloat(r.paid)||0; const pct = proposed > 0 ? Math.round(paid/proposed*100) : 0; const perf = calcContractorPerfScore(r); const perfColor = perf.total >= 80 ? 'var(--green)' : perf.total >= 60 ? 'var(--gold)' : perf.total >= 40 ? 'var(--orange)' : 'var(--red)'; const statusBg = (r.status||'Active')==='Active'?'var(--green)':(r.status||'Active')==='Completed'?'var(--blue)':(r.status||'Active')==='Mobilizing'?'var(--teal)':'var(--red)'; return ``; }).join('')}
#ContractorTrade / ScopeContract Value (UGX) Paid (UGX)Payment ProgressStatus PerformanceActions
${i+1} ${r.name} ${r.trade||'—'} ${proposed ? proposed.toLocaleString() : '—'} ${paid ? paid.toLocaleString() : '—'}
${pct}%
${r.status||'Active'} ${perfStars(perf.total)} ${perf.total}
`; } // ============================================================ // REPORTS & STATS — Zone Stats // ============================================================ function renderZonesStats() { const zones = getData('zones', []); const el = document.getElementById('zones-stats-content'); if (!el) return; const active = zones.filter(z => z.status === 'Active').length; const avgProg = zones.length ? Math.round(zones.reduce((s,z)=>s+(parseFloat(z.progress)||0),0)/zones.length) : 0; const totalWorkers = getData('erp_progress_staff_log', []).reduce((s,r)=>s+(r.count||0),0); const maxProg = Math.max(...zones.map(z=>parseFloat(z.progress)||0), 1); const bars = zones.map(z => { const pct = parseFloat(z.progress)||0; const h = Math.round((pct/maxProg)*160); const col = pct>=80?'green':pct>=50?'':'red'; return `
${pct}%
Zone ${z.id}
${(z.name||'').substring(0,10)}
`; }).join(''); el.innerHTML = `
${zones.length}
Total Zones
${active}
Active Zones
${avgProg}%
Avg Progress
${totalWorkers}
Total Worker-Days Logged

Zone Progress (%)

${bars||'

No zone data.

'}
`; } function printZonesStatsReport() { const zones = getData('zones', []); const w = window.open('','_blank','width=900,height=700'); w.document.write(rptHdr('Zones Progress Report','Infrastructure & Construction','Nedy Edrine Muziru','INF-ZST-001') + `${zones.map((z,i)=>``).join('')}
#ZoneStatusProgress %Workers On Site
${i+1}Zone ${z.id} — ${z.name||''}${z.status||'—'}${z.progress||0}%${z.workers||0}
`); w.document.close(); w.print(); } // ============================================================ // REPORTS & STATS — Contractors Stats // ============================================================ function renderContractorsStats() { const all = getMasterContractors(); const el = document.getElementById('contractors-stats-content'); if (!el) return; const total = all.reduce((s,r)=>s+(parseFloat(r.proposed)||0),0); const paid = all.reduce((s,r)=>s+(parseFloat(r.paid)||0),0); const active = all.filter(r=>(r.status||'Active')==='Active').length; const avgPerf = Math.round(all.reduce((s,r)=>s+calcContractorPerfScore(r).total,0)/Math.max(all.length,1)); // Top 10 by contract value const top10 = [...all].sort((a,b)=>(parseFloat(b.proposed)||0)-(parseFloat(a.proposed)||0)).slice(0,10); const maxVal = Math.max(...top10.map(r=>parseFloat(r.proposed)||0),1); const bars = top10.map(r => { const val = parseFloat(r.proposed)||0; const paidV = parseFloat(r.paid)||0; const h = Math.round((val/maxVal)*160); const pct = val>0?Math.round(paidV/val*100):0; return `
${(val/1e6).toFixed(0)}M
${r.name.substring(0,14)}
${pct}% paid
`; }).join(''); el.innerHTML = `
${all.length}
Total Contractors
${active}
Active
UGX ${(total/1e9).toFixed(2)}B
Total Contract Value
${total>0?Math.round(paid/total*100):0}%
Overall Pay Rate
${avgPerf}/100
Avg Performance

Top 10 Contractors by Contract Value (UGX millions)

${bars}
`; } function printContractorsStatsReport() { const all = getMasterContractors(); const w = window.open('','_blank','width=900,height=700'); w.document.write(rptHdr('Contractors Performance Report','Infrastructure & Construction','Nedy Edrine Muziru','INF-CST-001') + `${all.map((r,i)=>{ const p=parseFloat(r.proposed)||0, pd=parseFloat(r.paid)||0; return ``; }).join('')}
#ContractorTradeContract (UGX)Paid (UGX)Pay%StatusPerf Score
${i+1}${r.name}${r.trade||'—'}${p.toLocaleString()}${pd.toLocaleString()}${p>0?Math.round(pd/p*100):0}%${r.status||'Active'}${calcContractorPerfScore(r).total}/100
`); w.document.close(); w.print(); } // ============================================================ // REPORTS & STATS — Payments Stats // ============================================================ function renderPaymentsStats() { const payments = getData('contractorPayments', []); const el = document.getElementById('payments-stats-content'); if (!el) return; const all = getMasterContractors(); const totalPaid = payments.reduce((s,r)=>s+(parseFloat(r.amount)||0),0); const totalProposed = all.reduce((s,r)=>s+(parseFloat(r.proposed)||0),0); const outstanding = Math.max(0, totalProposed - totalPaid); const certs = [...new Set(payments.map(r=>r.ref||''))].filter(Boolean).length; // Payments per contractor chart const byContractor = {}; payments.forEach(p => { const k = p.contractor||'Other'; byContractor[k]=(byContractor[k]||0)+(parseFloat(p.amount)||0); }); const sorted = Object.entries(byContractor).sort((a,b)=>b[1]-a[1]).slice(0,10); const maxV = Math.max(...sorted.map(x=>x[1]),1); const bars = sorted.map(([name,val])=>{ const h = Math.round((val/maxV)*160); return `
${(val/1e6).toFixed(0)}M
${name.substring(0,14)}
`; }).join(''); el.innerHTML = `
${payments.length}
Payment Records
${certs}
Payment Certs
UGX ${(totalPaid/1e9).toFixed(2)}B
Total Paid
UGX ${(outstanding/1e9).toFixed(2)}B
Outstanding
${totalProposed>0?Math.round(totalPaid/totalProposed*100):0}%
Overall Paid Rate

Top 10 — Amount Paid per Contractor (UGX millions)

${bars||'

No payment records.

'}
`; } function printPaymentsStatsReport() { const payments = getData('contractorPayments', []); const w = window.open('','_blank','width=900,height=700'); w.document.write(rptHdr('Payments Statistics Report','Infrastructure & Construction','Nedy Edrine Muziru','INF-PST-001') + `${payments.map(r=>``).join('')}
DateContractorDescriptionAmount (UGX)RefStatus
${r.date||'—'}${r.contractor||'—'}${r.desc||'—'}${(parseFloat(r.amount)||0).toLocaleString()}${r.ref||'—'}${r.status||'—'}
`); w.document.close(); w.print(); } // ============================================================ // REPORTS & STATS — Equipment Stats // ============================================================ function renderEquipmentStats() { const equipment = getData('equipment', []); const el = document.getElementById('equipment-stats-content'); if (!el) return; const operational = equipment.filter(e=>e.status==='Operational').length; const breakdown = equipment.filter(e=>e.status==='Breakdown').length; const maintenance = equipment.filter(e=>e.status==='Maintenance').length; const overdue = equipment.filter(e => { if (!e.nextService) return false; return new Date(e.nextService) < new Date(); }).length; // By category const byCat = {}; equipment.forEach(e=>{const k=e.category||'Other'; byCat[k]=(byCat[k]||0)+1;}); const cats = Object.entries(byCat).sort((a,b)=>b[1]-a[1]); const maxC = Math.max(...cats.map(x=>x[1]),1); const bars = cats.map(([cat,cnt])=>{ const h = Math.round((cnt/maxC)*160); return `
${cnt}
${cat.substring(0,14)}
`; }).join(''); const statusBars = [ ['Operational',operational,'green'],['Maintenance',maintenance,''],['Breakdown',breakdown,'red'] ].map(([lbl,cnt,col])=>{ const h = Math.round((cnt/Math.max(equipment.length,1))*160); return `
${cnt}
${lbl}
`; }).join(''); el.innerHTML = `
${equipment.length}
Total Equipment
${operational}
Operational
${maintenance}
In Maintenance
${breakdown}
Breakdown
${overdue}
Overdue Service

By Category

${bars||'

No data.

'}

By Status

${statusBars}
`; } function printEquipmentStatsReport() { const equipment = getData('equipment', []); const w = window.open('','_blank','width=900,height=700'); w.document.write(rptHdr('Equipment Status Report','Infrastructure & Construction','Nedy Edrine Muziru','INF-EST-001') + `${equipment.map((e,i)=>``).join('')}
#EquipmentCategoryStatusZoneNext Service
${i+1}${e.name||'—'}${e.category||'—'}${e.status||'—'}${e.zone||'—'}${e.nextService||'—'}
`); w.document.close(); w.print(); } // ============================================================ // REPORTS & STATS — Site Log Stats // ============================================================ function renderSitelogStats() { const logs = getData('sitelog', []); const el = document.getElementById('sitelog-stats-content'); if (!el) return; const totalWorkers = logs.reduce((s,r)=>s+(parseInt(r.workers)||0),0); const byZone = {}; logs.forEach(r=>{const k=r.zone||'Unknown';byZone[k]=(byZone[k]||0)+(parseInt(r.workers)||0);}); const byWeather = {}; logs.forEach(r=>{const k=r.weather||'Unknown';byWeather[k]=(byWeather[k]||0)+1;}); const zones = Object.entries(byZone).sort((a,b)=>b[1]-a[1]); const maxW = Math.max(...zones.map(x=>x[1]),1); const zBars = zones.map(([z,w])=>{ const h = Math.round((w/maxW)*160); return `
${w}
${z.substring(0,14)}
`; }).join(''); const weathers = Object.entries(byWeather); const maxWe = Math.max(...weathers.map(x=>x[1]),1); const wBars = weathers.map(([w,cnt])=>{ const h = Math.round((cnt/maxWe)*160); return `
${cnt}
${w}
`; }).join(''); el.innerHTML = `
${logs.length}
Log Entries
${totalWorkers}
Total Workers Logged
${zones.length}
Zones Covered
${weathers.length}
Weather Conditions

Workers by Zone

${zBars||'

No data.

'}

Entries by Weather

${wBars||'

No data.

'}
`; } function printSitelogStatsReport() { const logs = getData('sitelog', []); const w = window.open('','_blank','width=900,height=700'); w.document.write(rptHdr('Site Log Statistics Report','Infrastructure & Construction','Nedy Edrine Muziru','INF-LST-001') + `${logs.map(r=>``).join('')}
DateZoneWeatherWorkersSupervisorNotes
${r.date||'—'}${r.zone||'—'}${r.weather||'—'}${r.workers||0}${r.supervisor||'—'}${r.notes||'—'}
`); w.document.close(); w.print(); } // ============================================================ // REPORTS & STATS — Snags Stats // ============================================================ function renderSnagsStats() { const snags = getData('snags', []); const el = document.getElementById('snags-stats-content'); if (!el) return; const open = snags.filter(s=>s.status!=='Closed').length; const closed = snags.filter(s=>s.status==='Closed').length; const critical = snags.filter(s=>(s.severity||s.priority||'')===('Critical'||'High')).length; const resolRate = snags.length > 0 ? Math.round(closed/snags.length*100) : 0; const bySeverity = {}; snags.forEach(s=>{const k=s.severity||s.priority||'Normal';bySeverity[k]=(bySeverity[k]||0)+1;}); const byZone = {}; snags.forEach(s=>{const k=s.zone||'Unknown';byZone[k]=(byZone[k]||0)+1;}); const sevs = Object.entries(bySeverity); const maxS = Math.max(...sevs.map(x=>x[1]),1); const sBars = sevs.map(([sev,cnt])=>{ const h = Math.round((cnt/maxS)*160); const col = sev==='Critical'||sev==='High'?'red':sev==='Medium'?'':'teal'; return `
${cnt}
${sev}
`; }).join(''); const zoneSorted = Object.entries(byZone).sort((a,b)=>b[1]-a[1]).slice(0,8); const maxZ = Math.max(...zoneSorted.map(x=>x[1]),1); const zBars = zoneSorted.map(([z,cnt])=>{ const h = Math.round((cnt/maxZ)*160); return `
${cnt}
${z.substring(0,14)}
`; }).join(''); el.innerHTML = `
${snags.length}
Total Snags
${open}
Open
${closed}
Closed
${critical}
Critical / High
${resolRate}%
Resolution Rate

By Severity

${sBars||'

No data.

'}

Snags by Zone

${zBars||'

No data.

'}
`; } function printSnagsStatsReport() { const snags = getData('snags', []); const w = window.open('','_blank','width=900,height=700'); w.document.write(rptHdr('Snag Register Report','Infrastructure & Construction','Nedy Edrine Muziru','INF-SST-001') + `${snags.map((s,i)=>``).join('')}
#DescriptionZoneContractorSeverityStatusTarget Date
${i+1}${s.desc||'—'}${s.zone||'—'}${s.contractor||'—'}${s.severity||s.priority||'—'}${s.status||'—'}${s.target||'—'}
`); w.document.close(); w.print(); } // ============================================================ // REPORTS & STATS — BOQ Stats // ============================================================ function renderBoqStats() { const el = document.getElementById('boq-stats-content'); if (!el) return; const cpData = BOQ_COFFEE_PARK || []; const nyabData = BOQ_NYABIHOKO || []; const allBoq = [...cpData, ...nyabData]; const totalValue = allBoq.reduce((s,r)=>s+(parseFloat(r.amount)||0),0); const contingency = allBoq.filter(r=>(r.desc||r.description||'').toLowerCase().includes('contingency')).reduce((s,r)=>s+(parseFloat(r.amount)||0),0); const buildings = allBoq.filter(r=>!(r.desc||r.description||'').toLowerCase().includes('contingency')).length; // By bill (group) const byBill = {}; allBoq.forEach(r=>{const k=r.bill||r.group||'General';byBill[k]=(byBill[k]||0)+(parseFloat(r.amount)||0);}); const bills = Object.entries(byBill).sort((a,b)=>b[1]-a[1]); const maxB = Math.max(...bills.map(x=>x[1]),1); const bars = bills.slice(0,10).map(([bill,val])=>{ const h = Math.round((val/maxB)*160); return `
${(val/1e9).toFixed(1)}B
${bill.substring(0,16)}
`; }).join(''); el.innerHTML = `
${allBoq.length}
BOQ Line Items
${buildings}
Buildings / Elements
UGX ${(totalValue/1e9).toFixed(2)}B
Total BOQ Value
UGX ${(contingency/1e6).toFixed(0)}M
Contingency

BOQ Value by Bill (UGX billions)

${bars||'

No BOQ data.

'}
`; } function printBoqStatsReport() { const allBoq = [...(BOQ_COFFEE_PARK||[]), ...(BOQ_NYABIHOKO||[])]; const w = window.open('','_blank','width=900,height=700'); w.document.write(rptHdr('Bill of Quantities Summary Report','Infrastructure & Construction','Nedy Edrine Muziru','INF-BOQ-001') + `${allBoq.map((r,i)=>``).join('')}
#DescriptionBill / GroupAmount (UGX)
${i+1}${r.desc||r.description||'—'}${r.bill||r.group||'—'}${(parseFloat(r.amount)||0).toLocaleString()}
`); w.document.close(); w.print(); } // ============================================================ // REPORTS & STATS — Supervision Stats // ============================================================ function renderSupervisionStats() { const el = document.getElementById('supervision-stats-content'); if (!el) return; // Supervision data from localStorage (erp_supervision) or fallback const supData = getData('erp_supervision', []); const ncrs = getData('erp_ncrs', []); const visits = getData('erp_site_visits', []); const bySpec = {}; supData.forEach(s=>{const k=s.specialisation||s.spec||'General';bySpec[k]=(bySpec[k]||0)+1;}); const specs = Object.entries(bySpec); const maxSpec = Math.max(...specs.map(x=>x[1]),1); const sBars = specs.map(([spec,cnt])=>{ const h = Math.round((cnt/maxSpec)*160); return `
${cnt}
${spec.substring(0,14)}
`; }).join(''); const openNcrs = ncrs.filter(n=>n.status!=='Closed').length; el.innerHTML = `
${supData.length}
Consultants
${visits.length}
Site Visits Logged
${ncrs.length}
NCRs Issued
${openNcrs}
Open NCRs

Consultants by Specialisation

${sBars||'

No supervision consultant data recorded yet.

'}
`; } function printSupervisionStatsReport() { const supData = getData('erp_supervision', []); const w = window.open('','_blank','width=900,height=700'); w.document.write(rptHdr('Supervision Consultants Report','Infrastructure & Construction','Nedy Edrine Muziru','INF-SUP-001') + `${supData.length ? supData.map((s,i)=>``).join('') : ''}
#ConsultantSpecialisationCompanyStatus
${i+1}${s.name||'—'}${s.specialisation||s.spec||'—'}${s.company||'—'}${s.status||'Active'}
No supervisor records.
`); w.document.close(); w.print(); } // ============================================================ // REPORTS & STATS — Progress Report Stats // ============================================================ function renderProgressStats() { const mainData = getProgressMain(); const nyabData = getProgressNyabihoko(); const allData = [...mainData, ...nyabData]; const el = document.getElementById('progress-stats-content'); if (!el) return; const byStatus = {}; allData.forEach(r=>{const k=r.status||'Unknown';byStatus[k]=(byStatus[k]||0)+1;}); const statuses = Object.entries(byStatus); const maxSt = Math.max(...statuses.map(x=>x[1]),1); const avgProg = allData.length ? Math.round(allData.reduce((s,r)=>s+(parseFloat(r.progress)||0),0)/allData.length) : 0; const complete = allData.filter(r=>(parseFloat(r.progress)||0)>=100).length; const halted = allData.filter(r=>(r.status||'').toLowerCase().includes('halt')||(r.status||'').toLowerCase().includes('suspend')).length; const stBars = statuses.map(([st,cnt])=>{ const h = Math.round((cnt/maxSt)*160); const col = st.toLowerCase().includes('complet')?'green':st.toLowerCase().includes('halt')||st.toLowerCase().includes('suspend')?'red':st.toLowerCase().includes('progress')?'':'teal'; return `
${cnt}
${st.substring(0,14)}
`; }).join(''); // Progress % distribution buckets const buckets = {'0%':0,'1-25%':0,'26-50%':0,'51-75%':0,'76-99%':0,'100%':0}; allData.forEach(r=>{ const p = parseFloat(r.progress)||0; if(p===0)buckets['0%']++; else if(p<=25)buckets['1-25%']++; else if(p<=50)buckets['26-50%']++; else if(p<=75)buckets['51-75%']++; else if(p<100)buckets['76-99%']++; else buckets['100%']++; }); const maxBk = Math.max(...Object.values(buckets),1); const bkBars = Object.entries(buckets).map(([lbl,cnt])=>{ const h = Math.round((cnt/maxBk)*160); const col = lbl==='100%'?'green':lbl==='0%'?'red':lbl.startsWith('76')?'teal':''; return `
${cnt}
${lbl}
`; }).join(''); el.innerHTML = `
${allData.length}
Total Work Items
${mainData.length}
Coffee Park Site
${nyabData.length}
Nyabihoko Site
${avgProg}%
Avg Progress
${complete}
Complete (100%)
${halted}
Halted / Suspended

Work Items by Status

${stBars||'

No data.

'}

Progress % Distribution

${bkBars}
`; } function printProgressStatsReport() { const mainData = getProgressMain(); const nyabData = getProgressNyabihoko(); const allData = [...mainData, ...nyabData]; const w = window.open('','_blank','width=900,height=700'); w.document.write(rptHdr('Progress Report — Statistics','Infrastructure & Construction','Nedy Edrine Muziru','INF-PRS-001') + `${allData.map((r,i)=>``).join('')}
#SiteBuilding / ElementProgress %StatusContractor
${i+1}${i${r.building||r.element||'—'}${r.progress||0}%${r.status||'—'}${r.contractor||'—'}
`); w.document.close(); w.print(); } `); w.document.close(); } // ── Sub-tab 4: IPC Register ──────────────────────────────────── function renderIpcRegister() { const ipcs = getIpcData(); const tbody = document.getElementById('ipc-reg-tbody'); const kpis = document.getElementById('ipc-reg-kpis'); if (!tbody) return; const search = ((document.getElementById('ipc-reg-search')||{}).value||'').toLowerCase(); const statusFlt = (document.getElementById('ipc-reg-status')||{}).value||''; // KPIs const totCerts = ipcs.length; const totCertVal = ipcs.reduce((s,i)=>s+(i.netThisCert||0),0); const totPaid = ipcs.filter(i=>i.status==='Paid').reduce((s,i)=>s+(i.paidAmount||i.netThisCert||0),0); const totOutst = ipcs.filter(i=>i.status!=='Paid').reduce((s,i)=>s+(i.netThisCert||0),0); const totRet = ipcs.reduce((s,i)=>s+(i.retention||0),0); if (kpis) kpis.innerHTML = `
Certificates Issued
${totCerts}
Total Certified
${fmtUGX(totCertVal)}
Total Paid
${fmtUGX(totPaid)}
Outstanding
${fmtUGX(totOutst)}
Retention Held
${fmtUGX(totRet)}
`; const filtered = ipcs.filter(ipc => { if (search && !(ipc.contractor||'').toLowerCase().includes(search) && !(ipc.ipcNo||'').toLowerCase().includes(search)) return false; if (statusFlt && ipc.status !== statusFlt) return false; return true; }); if (filtered.length === 0) { tbody.innerHTML = `No IPCs found.`; return; } tbody.innerHTML = filtered.map((ipc,i) => ` ${ipc.ipcNo} ${ipc.createdAt||'—'} ${ipc.contractor} ${ipc.building||'—'} ${(ipc.grossToDate||0).toLocaleString()} ${(ipc.netThisCert||0).toLocaleString()} ${(ipc.retention||0).toLocaleString()} ${ipcStatusBadge(ipc.status)} ${ipc.msRef||'—'} `).join(''); } function viewIpcFromRegister(idx) { const ipc = getIpcData()[idx]; if (!ipc) return; showSub('ipc','generator', document.querySelector('#mod-ipc .sub-tab:nth-child(2)')); setTimeout(() => { const setVal = (id, v) => { const el = document.getElementById(id); if (el) el.value = v; }; setVal('ipc-no', ipc.ipcNo); setVal('ipc-contractor', ipc.contractor); setVal('ipc-contract-no', ipc.contractNo); setVal('ipc-contract-sum', ipc.contractSum); setVal('ipc-period-from', ipc.periodFrom); setVal('ipc-period-to', ipc.periodTo); setVal('ipc-desc', ipc.building); setVal('ipc-gross-to-date', ipc.grossToDate); setVal('ipc-retention-pct', ipc.retentionPct); setVal('ipc-retention-amt', ipc.retention); setVal('ipc-amt-due-to-date',ipc.amtDueToDate); setVal('ipc-prev-certified', ipc.prevCertified); setVal('ipc-net-cert', ipc.netThisCert); setVal('ipc-site-engineer', ipc.siteEngineer); setVal('ipc-qs', ipc.qs); setVal('ipc-pm', ipc.pm); setVal('ipc-ceo', ipc.ceo); }, 100); } function printIpcFromRegister(idx) { const ipc = getIpcData()[idx]; if (!ipc) return; const w = window.open('','_blank'); if (!w) return; const docHtml = buildIpcDocHtml(ipc); w.document.write(`${ipc.ipcNo} ${docHtml} `); w.document.close(); } // ── Sub-tab 5: Retention Ledger ──────────────────────────────── function renderRetentionLedger() { const ipcs = getIpcData(); const master = getMasterContractors(); const tbody = document.getElementById('ret-tbody'); const strip = document.getElementById('ret-kpi-strip'); if (!tbody) return; // Aggregate per contractor const retMap = {}; ipcs.forEach(ipc => { if (!retMap[ipc.contractor]) retMap[ipc.contractor] = {held:0,released:0}; retMap[ipc.contractor].held += (ipc.retention||0); // If paid, half of retention is releasable at practical completion }); const contractors = Object.keys(retMap); const totHeld = contractors.reduce((s,c)=>s+retMap[c].held,0); const totReleased = contractors.reduce((s,c)=>s+retMap[c].released,0); if (strip) strip.innerHTML = `
Contractors with Retention
${contractors.length}
Total Held
${fmtUGX(totHeld)}
Released
${fmtUGX(totReleased)}
`; if (contractors.length === 0) { tbody.innerHTML = `No retention data. IPCs must be generated first.`; return; } tbody.innerHTML = contractors.map(ctrName => { const ret = retMap[ctrName]; const ctr = master.find(c=>c.name===ctrName)||{}; const cs = ctr.contract_sum||0; const paid = ctr.paid||0; const pct = cs > 0 ? Math.round(paid/cs*100) : 0; const releaseCond = pct >= 90 ? 'Met — Practical Completion' : `Not yet (${pct}% complete)`; const condClass = pct >= 90 ? 'color:var(--green);font-weight:700' : 'color:var(--orange)'; return ` ${ctrName} ${ret.held.toLocaleString()}
${pct}% ${releaseCond} ${ret.released ? ret.released.toLocaleString() : '—'} ${(ret.held-ret.released).toLocaleString()} ${pct>=90?``:'Pending completion'} `; }).join(''); } // Retention release state let _retReleaseCtr = ''; let _retReleaseAmt = 0; function openReleaseRetention(ctrName, heldAmt) { _retReleaseCtr = ctrName; _retReleaseAmt = heldAmt; const content = document.getElementById('ret-release-content'); if (content) content.innerHTML = `

Release retention for:
${ctrName}

`; const modal = document.getElementById('ret-release-modal'); if (modal) modal.classList.remove('hidden'); } function confirmReleaseRetention() { const amt = parseFloat((document.getElementById('ret-release-amount')||{}).value)||0; const date = (document.getElementById('ret-release-date')||{}).value||''; if (!amt || !date) { alert('Amount and date required.'); return; } alert('Retention Release Certificate generated for ' + _retReleaseCtr + ' — UGX ' + amt.toLocaleString() + ' on ' + date + '.\n\nIn production, this would generate a Retention Release Certificate document.'); closeModal('ret-release-modal'); renderRetentionLedger(); } // ── Modal helpers ───────────────────────────────────────────── // (stub openModal removed — canonical async version defined above handles all modal population) function closeModal(id) { const el = document.getElementById(id); if (el) el.classList.add('hidden'); }