All projects
Agent-built productJuly 2026Reported by the owner

Reproduces the feasibility study to the rupee

Built for

the founders of Ahbab Health Education Institute, Islamabad

Project

Ahbab feasibility builder

Investment, 21 programmes with real Sarhad University fees, enrolment by year, scholarships, staffing roster, best and worst scenarios, then profit, ROI, IRR, and payback with a full P&L. No build, no server, works offline.

02

Demo

ahbab-feasibility-dashboard.vercel.app
open ↗

Live. Walk the seven steps; every number recomputes.

03

How it works

Architecture diagram
  1. 01

    One HTML file so it can be emailed and opened anywhere, including offline.

  2. 02

    The revenue engine was validated against the source study's student counts and gross fees before any UI work.

  3. 03

    IRR solved by bisection in a few lines rather than a library.

  4. 04

    Three scenarios computed from one set of inputs plus income and cost swings.

04

Screenshots

Step 1 of 7: investment
Step 1 of 7: investment
05

Stack and code

Ahbab-Feasibility-Dashboard.html
    case 'annualEach':{const i=Math.min(T-1,e.values.length-1);return e.values[i]||0;}
    case 'monthly':{let m=e.perMonth||0;const esc=(e.esc||0)/100,st=e.escStart||1;if(esc&&T>=st)m=(e.perMonth||0)*Math.pow(1+esc,T-st+1);return m*12;}
    case 'perStudent':{const a=e.scope==='health'?ctx.base.health:e.scope==='nonhealth'?ctx.base.nonh:ctx.base.students;return (a[T-1]||0)*(e.amount||0);}
    case 'pctIncome':{if(T===1&&e.minY1)return e.minY1;return (e.pct||0)/100*(ctx.revenue[T-1]||0);}
    case 'perProgramme':return (e.amount||0)*ctx.base.nProg;
    case 'fixed':return e.amount||0;
    case 'oneTimeYear':return T===(e.year||1)?(e.amount||0):0;
    default:return 0;
  }
}
function computeScenario(base,incMult,costMult,investment,taxRate){
  const h=base.h,revenue=base.revenue.map(r=>r*incMult);
  const ctx={base,revenue};
  const totalCost=Array(h).fill(0);
  // Salaries come from the staffing roster and lead the cost lines.
  const salArr=Array(h).fill(0);
  for(let T=1;T<=h;T++){let v=staffBillYear(T);if(staffSwing)v*=costMult;salArr[T-1]=v;totalCost[T-1]+=v;}
  const costLines=[{label:'Salaries',arr:salArr,swing:staffSwing}];
  EXPENSES.forEach(e=>{const arr=Array(h).fill(0);for(let T=1;T<=h;T++){let v=expenseYear(e,T,ctx);if(e.swing)v*=costMult;arr[T-1]=v;totalCost[T-1]+=v;}costLines.push({label:e.label,arr,swing:e.swing});});
  const pbt=[],tax=[],pat=[];let loss=0;
  for(let i=0;i<h;i++){const p=revenue[i]-totalCost[i];let t=0;if(p<=0){loss+=-p;}else{const off=Math.min(p,loss);loss-=off;t=taxRate*(p-off);}pbt.push(p);tax.push(t);pat.push(p-t);}
  const total5=pat.reduce((a,b)=>a+b,0),cumul=[];let c=0;pat.forEach(v=>{c+=v;cumul.push(c);});
  return {revenue,costLines,totalCost,pbt,tax,pat,total5,cumul,fees:base.fees,schol:base.schol,students:base.students,
    roi:investment>0?total5/investment:0,irr:irr([-investment,...pat]),payback:payback(cumul,investment),y5margin:revenue[h-1]>0?pat[h-1]/revenue[h-1]:0};
}
function irr(cf){const f=r=>cf.reduce((a,c,t)=>a+c/Math.pow(1+r,t),0);let lo=-0.9,hi=5;if(f(lo)*f(hi)>0)return null;for(let k=0;k<100;k++){const mid=(lo+hi)/2;if(f(mid)>0){lo=mid;}else{hi=mid;}}return (lo+hi)/2;}
function payback(cumul,inv){for(let i=0;i<cumul.length;i++){if(cumul[i]>=inv){const prev=i===0?0:cumul[i-1];const d=cumul[i]-prev;return i+(d>0?(inv-prev)/d:0);}}return null;}

function computeAll(){
  reconcile();
  const h=H(),ids=activeIds();
  if(ids.length===0)return {empty:true,h};
  const base=computeBase(h,ids);
  const inv=num('investment'),tax=num('taxRate')/100;
  const incUp=num('incUp')/100,costDown=num('costDown')/100,incDown=num('incDown')/100,costUp=num('costUp')/100;
  const scs={best:computeScenario(base,1+incUp,1-costDown,inv,tax),exp:computeScenario(base,1,1,inv,tax),worst:computeScenario(base,1-incDown,1+costUp,inv,tax)};
  return {empty:false,h,ids,base,scs,inv,tax};
}

/* ===== CHARTS ===== */
const C={navy:'#1f3a5f',navy2:'#2c5079',gold:'#c8a24a',goldd:'#a8842f',green:'#3f7d54',red:'#b0433c',grey:'#9aa7b4',mut:'#66707a',line:'#e2e6ea'};
const svgOpen=(w,h)=>`<svg viewBox="0 0 ${w} ${h}" width="100%" style="max-width:${w}px;height:auto;font-family:inherit">`;
const txt=(x,y,t,o={})=>`<text x="${x}" y="${y}" font-size="${o.s||11}" fill="${o.c||C.mut}" text-anchor="${o.a||'middle'}" ${o.b?'font-weight="700"':''}>${t}</text>`;
function barProfit(scs,inv){
  const w=680,h=300,pad={l:58,r:16,t:24,b:44},iw=w-pad.l-pad.r,ih=h-pad.t-pad.b;
  const data=[scs.best.total5,scs.exp.total5,scs.worst.total5],cols=[C.green,C.navy,C.red],labels=['Best','Expected','Worst'];
  const max=Math.max(...data,inv,1)*1.14,min=Math.min(0,...data);const y=v=>pad.t+ih-((v-min)/(max-min))*ih,gap=iw/3,bw=gap*0.5;
  let g=svgOpen(w,h);
  for(let k=0;k<=4;k++){const v=min+(max-min)*k/4,yy=y(v);g+=`<line x1="${pad.l}" x2="${w-pad.r}" y1="${yy}" y2="${yy}" stroke="${C.line}"/>`+txt(pad.l-8,yy+3,fM(v),{a:'end'});}
  g+=`<line x1="${pad.l}" x2="${w-pad.r}" y1="${y(0)}" y2="${y(0)}" stroke="#c9ced3"/>`;
  data.forEach((v,i)=>{const cx=pad.l+gap*i+gap/2,yy=y(Math.max(0,v)),y0=y(Math.min(0,v));g+=`<rect x="${cx-bw/2}" y="${Math.min(yy,y0)}" width="${bw}" height="${Math.abs(y0-yy)}" rx="3" fill="${cols[i]}"/>`+txt(cx,(v>=0?yy-7:y0+15),fM(v),{b:true,c:cols[i],s:13})+txt(cx,h-22,labels[i],{s:12,c:C.mut,b:true});});
  const yl=y(inv);g+=`<line x1="${pad.l}" x2="${w-pad.r}" y1="${yl}" y2="${yl}" stroke="${C.gold}" stroke-width="2" stroke-dasharray="7 5"/>`+txt(w-pad.r,yl-6,'Investment '+fM(inv),{a:'end',c:C.goldd,b:true,s:11});
  return g+'</svg>';
}
function lineCumul(scs,inv,h){
  const w=680,H2=310,pad={l:58,r:20,t:20,b:40},iw=w-pad.l-pad.r,ih=H2-pad.t-pad.b;
  const series=[['worst',C.red],['exp',C.navy],['best',C.green]];let allv=[inv,0];series.forEach(([k])=>allv.push(...scs[k].cumul));
  const max=Math.max(...allv)*1.08,min=Math.min(0,...allv);const X=i=>pad.l+(h<=1?0:iw*i/(h-1)),Y=v=>pad.t+ih-((v-min)/(max-min))*ih;
  let g=svgOpen(w,H2);
  for(let k=0;k<=4;k++){const v=min+(max-min)*k/4,yy=Y(v);g+=`<line x1="${pad.l}" x2="${w-pad.r}" y1="${yy}" y2="${yy}" stroke="${C.line}"/>`+txt(pad.l-8,yy+3,fM(v),{a:'end'});}
  for(let i=0;i<h;i++)g+=txt(X(i),H2-16,'Year '+(i+1),{s:11});

github/ahbab-feasibility-dashboard/Ahbab-Feasibility-Dashboard.html