// ─── FEED MATRIX ───────────────────────────────────────────────────────────── const FEED_MATRIX = { wheat_straw: {name:"Wheat Straw (Bhusa)", dm:0.90, cp:0.030, tdn:0.50,ndf:0.742,ca:0.0030, p:0.0010, cost:8, cat:"Roughage", maxWetKg:99}, paddy_straw: {name:"Paddy Straw", dm:0.90, cp:0.046, tdn:0.60,ndf:0.662,ca:0.0025, p:0.0010, cost:8, cat:"Roughage", maxWetKg:99}, cultivated_maize: {name:"Fresh Maize Fodder", dm:0.22, cp:0.082, tdn:0.78,ndf:0.525,ca:0.0042, p:0.0014, cost:7, cat:"Green Cereal",maxWetKg:99}, napier_grass: {name:"Napier Grass (Mature)", dm:0.22, cp:0.092, tdn:0.58,ndf:0.717,ca:0.0030, p:0.0018, cost:5, cat:"Green Cereal",maxWetKg:99}, maize_silage: {name:"Maize Silage", dm:0.30, cp:0.079, tdn:0.68,ndf:0.460,ca:0.0042, p:0.0014, cost:8, cat:"Silage", maxWetKg:99}, jowar_green: {name:"Jowar Green Fodder", dm:0.22, cp:0.070, tdn:0.65,ndf:0.550,ca:0.0050, p:0.0020, cost:7, cat:"Green Cereal",maxWetKg:99}, berseem: {name:"Berseem (Green Clover)", dm:0.15, cp:0.174, tdn:0.72,ndf:0.424,ca:0.0200, p:0.0038, cost:5, cat:"Green Legume",maxWetKg:6.0}, hmf: {name:"Hydroponic Maize Fodder", dm:0.2207,cp:0.1427,tdn:0.72,ndf:0.180,ca:0.00105,p:0.004129,cost:9, cat:"Hydroponic", maxWetKg:99}, commercial_feed: {name:"Cattle Feed (BIS Type II)", dm:0.90, cp:0.200, tdn:0.72,ndf:0.300,ca:0.0070, p:0.0050, cost:28, cat:"Concentrate", maxWetKg:99}, cottonseed_cake: {name:"Cottonseed Cake (Binola Khali)",dm:0.90, cp:0.410, tdn:0.70,ndf:0.280,ca:0.0017, p:0.0060, cost:22, cat:"Concentrate", maxWetKg:2.0}, mustard_cake: {name:"Mustard Cake (Sarson Khali)", dm:0.90, cp:0.380, tdn:0.72,ndf:0.256,ca:0.0073, p:0.0085, cost:32, cat:"Concentrate", maxWetKg:2.0}, wheat_bran: {name:"Wheat Bran (Chokar)", dm:0.88, cp:0.160, tdn:0.70,ndf:0.640,ca:0.0017, p:0.0093, cost:28, cat:"Concentrate", maxWetKg:3.0}, }; const FEEDS = Object.entries(FEED_MATRIX).map(([key,f])=>({ key,name:f.name,dm:f.dm,cp:f.cp*100,tdn:f.tdn*100,ndf:f.ndf*100, ca:f.ca*100,p:f.p*100,cost:f.cost,cat:f.cat,maxWetKg:f.maxWetKg, desc:{ wheat_straw:"Dry cereal straw, primary rumen mat. Low energy, essential for gut motility.", paddy_straw:"Rice straw, moderately high NDF. Good roughage base in paddy-growing regions.", cultivated_maize:"High-energy green fodder. Widely grown across India, good palatability.", napier_grass:"Pennisetum purpureum. High-yield perennial. Mature CP 9.2%, NDF 71.7%.", maize_silage:"Fermented maize, excellent energy and fibre balance. Year-round availability.", jowar_green:"Sorghum green fodder. Good calcium source. Common in Deccan and central India.", berseem:"Highest protein green fodder. Rich in Ca. Cap at 6 kg wet/day for palatability.", hmf:"HMF by Shunya Fodder & Forage Agritech. Lab-tested: 14.27% CP, 0.105% Ca, 0.413% P (SGS GG25-007509).", commercial_feed:"Compound cattle feed meeting BIS 1974 Type II norms. Balanced concentrate.", cottonseed_cake:"High protein oil cake. Max 2 kg/day wet. Avoid in late pregnancy (gossypol).", mustard_cake:"Rich in protein and phosphorus. High cost but excellent amino acid profile.", wheat_bran:"Excellent phosphorus source. Laxative effect at high doses. Max 3 kg/day.", }[key]||"" })); const ASMM = {ca_pct:0.20, p_pct:0.12, cost_per_g:0.15}; const NDF_MIN = 0.28; let customFeedCosts={}; Object.entries(FEED_MATRIX).forEach(([k,f])=>{customFeedCosts[k]=f.cost;}); customFeedCosts['asmm']=ASMM.cost_per_g; const feedImages={ wheat_straw:"https://www.shunya.live/wp-content/uploads/2026/05/Shunya-Agritech-Wheat-Straw-Bhusa.png", paddy_straw:"https://www.shunya.live/wp-content/uploads/2026/05/Shunya-Agritech-Paddy-Straw.jpeg", cultivated_maize:"https://www.shunya.live/wp-content/uploads/2026/05/Shunya-Agritech-Maize-Green-Forage.jpg", napier_grass:"https://www.shunya.live/wp-content/uploads/2026/05/Shunya-Agritech-hybrid-napier-bajra-grass.jpg", jowar_green:"https://www.shunya.live/wp-content/uploads/2026/05/Shunya-Agritech-Jowar-Green-Grass.jpeg", berseem:"https://www.shunya.live/wp-content/uploads/2026/05/Shunya-Agritech-Berseem-Barseem.jpg", maize_silage:"https://www.shunya.live/wp-content/uploads/2026/05/Shunya-Agritech-corn-silage-cattle-feed.jpg", hmf:"https://www.shunya.live/wp-content/uploads/2026/03/53d2a11b-f422-4b6b-b8fb-aa07e4ee8124-removebg-preview.png", commercial_feed:"https://www.shunya.live/wp-content/uploads/2026/05/Shunya-Agritech-Cattle-Feed.jpg", cottonseed_cake:"https://www.shunya.live/wp-content/uploads/2026/05/Shunya-Agritech-Cottonseed-Cake.jpg", mustard_cake:"https://www.shunya.live/wp-content/uploads/2026/05/Shunya-Agritech-Mustard-Cake.jpg", wheat_bran:"https://www.shunya.live/wp-content/uploads/2026/05/Shunya-Agritech-wheat-bran-for-animal-feed-663.jpg", }; const SOURCES=[ {title:"NDDB RBP — Feed Calculator Logic",desc:"Factorial method formulas, worked example",pdfUrl:"https://www.shunya.live/wp-content/uploads/2026/05/Feed-Calculator-Logic.pdf"}, {title:"Animal Nutrition Booklet — NDDB 2012",desc:"Feed composition tables: CP, NDF, Ca, P",pdfUrl:"https://www.shunya.live/wp-content/uploads/2026/05/Animal-Nutrition-booklet-1.pdf"}, {title:"FAO Paper 173 — Garg 2012",desc:"SARA limits — rumen acidity guidelines",pdfUrl:"https://www.shunya.live/wp-content/uploads/2026/05/i3014e00.pdf"}, {title:"NDDB Compound Cattle Feed Norm",desc:"Daily cattle feed norms by body weight",pdfUrl:"https://www.shunya.live/wp-content/uploads/2026/05/Compound_Cattle_Feed1.pdf"}, {title:"ICAR 2013 Nutrient Requirements",desc:"Indian standard nutrient requirement tables",pdfUrl:"https://www.shunya.live/wp-content/uploads/2026/05/Mineral_Mixture1.pdf"}, {title:"SGS India Lab Report GG25-007509",desc:"HMF composition: protein, Ca, P by Shunya Fodder",pdfUrl:"https://www.shunya.live/wp-content/uploads/2026/05/Report_maize-8aug-1.pdf"}, ]; const CAT_GROUPS=[ {label:"Dry Roughage",cats:["Roughage"]}, {label:"Green Fodder",cats:["Green Cereal","Green Legume"]}, {label:"Silage",cats:["Silage"]}, {label:"Hydroponic",cats:["Hydroponic"]}, {label:"Concentrates",cats:["Concentrate"]}, ]; let baselineChecked={}, customQtyMap={}, customLockMap={}, milkPricePerLitre=null; let goalsState=null, baselineResult=null, customResult=null; // ─── GOALS ─────────────────────────────────────────────────────────────────── function computeGoals(){ const animalType=document.getElementById('animalType').value; const lifeStage=document.getElementById('lifeStage').value; const bw=parseFloat(document.getElementById('bodyWeight').value)||450; const milk=lifeStage==='lactating'?parseFloat(document.getElementById('milkYield').value)||0:0; const fat=lifeStage==='lactating'?parseFloat(document.getElementById('fatPct').value)||4.0:0; const mbw=Math.pow(bw,0.75); let dmiTarget,dmiMax,tdnTargetG,cpTargetG,caTargetG,pTargetG; if(lifeStage==='calf'){ dmiTarget=dmiMax=bw*0.022; tdnTargetG=bw*12; cpTargetG=bw*4.5; caTargetG=bw*0.35; pTargetG=bw*0.20; } else if(lifeStage==='pregnant_t1'){ dmiTarget=dmiMax=bw*0.020; tdnTargetG=35*mbw; cpTargetG=5.0*mbw; caTargetG=(animalType==='buffalo'?4.5:4.0)*bw/100; pTargetG=2.0*bw/100; } else if(lifeStage==='pregnant_t2'){ dmiTarget=dmiMax=bw*0.020; tdnTargetG=35*mbw+700; cpTargetG=5.0*mbw+75; caTargetG=(animalType==='buffalo'?4.5:4.0)*bw/100+5; pTargetG=2.0*bw/100+3; } else if(lifeStage==='pregnant_t3'){ dmiTarget=dmiMax=bw*0.020; tdnTargetG=35*mbw+1500; cpTargetG=5.0*mbw+150; caTargetG=(animalType==='buffalo'?4.5:4.0)*bw/100+11; pTargetG=2.0*bw/100+6.5; } else if(lifeStage==='dry'){ dmiTarget=dmiMax=bw*0.018; tdnTargetG=35*mbw; cpTargetG=5.0*mbw; caTargetG=(animalType==='buffalo'?4.5:4.0)*bw/100; pTargetG=2.0*bw/100; } else { if(animalType==='buffalo'){ const fcm6=milk*(0.4+0.15*fat)/1.3; tdnTargetG=37*mbw+368*fcm6; cpTargetG=5.2*mbw+76*fcm6; caTargetG=4.5*bw/100+3.2*milk; pTargetG=2.0*bw/100+2.6*milk; dmiTarget=bw*0.025+milk*0.075; dmiMax=bw*0.025+milk*0.15; } else if(animalType==='cow_indigenous'){ const fcm4=milk*(0.4+0.15*fat); tdnTargetG=35*mbw+236*fcm4; cpTargetG=5.0*mbw+57*fcm4; caTargetG=4.0*bw/100+2.8*milk; pTargetG=2.0*bw/100+2.2*milk; dmiTarget=bw*0.022+milk*0.140; dmiMax=bw*0.022+milk*0.200; } else { const fcm4=milk*(0.4+0.15*fat); tdnTargetG=35*mbw+298*fcm4; cpTargetG=5.0*mbw+63*fcm4; caTargetG=4.0*bw/100+2.4*milk; pTargetG=2.0*bw/100+1.9*milk; dmiTarget=bw*0.025+milk*0.025; dmiMax=bw*0.025+milk*0.10; } } return {bw,milk,fat,animalType,lifeStage,mbw,dmiTarget,dmiMax,tdnTargetG,cpTargetG,caTargetG,pTargetG}; } function computeAsmm(caFeedD,pFeedD,caTarget,pTarget,bw){ const ca_gap=Math.max(0,caTarget-caFeedD); const p_gap=Math.max(0,pTarget-pFeedD); if(ca_gap<=0&&p_gap<=0) return {dose_g:0,ca:0,p:0,ca_gap:0,p_gap:0,caDriver:'none'}; const doseRaw=Math.max(ca_gap/ASMM.ca_pct, p_gap/ASMM.p_pct); const dynamicCeiling=bw?Math.min(300,bw*0.4):200; const dose_g=Math.min(dynamicCeiling,Math.ceil(doseRaw/10)*10); const caDriver=(ca_gap/ASMM.ca_pct)>=(p_gap/ASMM.p_pct)?'Ca':'P'; return {dose_g,ca:dose_g*ASMM.ca_pct,p:dose_g*ASMM.p_pct,ca_gap,p_gap,caDriver}; } function runSolverCore(goals,lockedOverrides,checkedKeys){ const{dmiTarget,dmiMax,tdnTargetG,cpTargetG,bw,milk,lifeStage}=goals; const AF={}; for(const key of(checkedKeys||Object.keys(FEED_MATRIX))){ const fm=FEED_MATRIX[key];if(!fm)continue; const isLocked=!!(lockedOverrides&&lockedOverrides[key]!==undefined); AF[key]={...fm,isLocked,forcedWet:isLocked?lockedOverrides[key]:0}; } let schedule={},dmiD=0,tdnD=0,cpD=0,ndfD=0,caFeedD=0,pFeedD=0; function add(key,wet){ if(wet<=0||!AF[key])return false; const f=AF[key];const dm=wet*f.dm; if(dmiD+dm>dmiMax*1.001)return false; if((schedule[key]||0)+wet>(f.maxWetKg||99)*1.001)return false; schedule[key]=(schedule[key]||0)+wet; dmiD+=dm;tdnD+=dm*f.tdn*1000;cpD+=dm*f.cp*1000; ndfD+=dm*f.ndf;caFeedD+=dm*f.ca*1000;pFeedD+=dm*f.p*1000; return true; } const freeOf=cat=>Object.keys(AF).filter(k=>!AF[k].isLocked&&AF[k].cat===cat); const freeConc=()=>freeOf('Concentrate'); for(const k in AF){ if(!AF[k].isLocked)continue; const wet=AF[k].forcedWet; const allowed=Math.min(wet,Math.max(0,(dmiMax-dmiD)/AF[k].dm)); if(allowed>0.001){ schedule[k]=(schedule[k]||0)+allowed; const d=allowed*AF[k].dm; dmiD+=d;tdnD+=d*AF[k].tdn*1000;cpD+=d*AF[k].cp*1000; ndfD+=d*AF[k].ndf;caFeedD+=d*AF[k].ca*1000;pFeedD+=d*AF[k].p*1000; } } const roughFloorDm=(lifeStage==='calf'?bw*0.0045:bw*0.009); const alreadyRoughDm=Object.keys(AF).filter(k=>AF[k].isLocked&&AF[k].cat==='Roughage').reduce((s,k)=>s+AF[k].forcedWet*AF[k].dm,0); const roughNeedDm=Math.max(0,roughFloorDm-alreadyRoughDm); const fr=freeOf('Roughage').sort((a,b)=>AF[a].cost-AF[b].cost); if(fr.length>0&&roughNeedDm>0.05){ if(fr.length===1)add(fr[0],roughNeedDm/AF[fr[0]].dm); else{add(fr[0],roughNeedDm*0.70/AF[fr[0]].dm);add(fr[1],roughNeedDm*0.30/AF[fr[1]].dm);} } const roughTdnEst=bw*0.009*(0.70*0.50+0.30*0.60)*1000; const maxGreenTdn=Math.max(0,tdnTargetG*0.68-roughTdnEst); const greenTdnPerKg=0.80*0.22*0.78*1000+0.20*0.15*0.72*1000; const fullGBbyTDN=maxGreenTdn/greenTdnPerKg; const fullGB=Math.min(bw*0.030+milk*0.50,dmiTarget*1.2,fullGBbyTDN); const gCereals=[...freeOf('Green Cereal'),...freeOf('Silage')].sort((a,b)=>(AF[b].tdn-AF[a].tdn)||(AF[a].cost-AF[b].cost)); const gLegumes=freeOf('Green Legume').sort((a,b)=>AF[a].cost-AF[b].cost); if(gCereals.length>0&&gLegumes.length>0){add(gCereals[0],fullGB*0.80);add(gLegumes[0],fullGB*0.20);} else if(gCereals.length>0){add(gCereals[0],fullGB);} else if(gLegumes.length>0){add(gLegumes[0],Math.min(fullGB*0.50,AF[gLegumes[0]].maxWetKg||99));} const freeHmf=freeOf('Hydroponic'); if(freeHmf.length>0&&gCereals.length===0){add(freeHmf[0],fullGB*0.80);} const concNormWet=lifeStage==='calf'?0:(2.75+milk*0.4); const concNormDmCeil=lifeStage==='calf'?dmiMax*0.30:concNormWet*0.92; const concPool=freeConc(); const cappedC=concPool.filter(k=>(AF[k].maxWetKg||99)<=10); const sumCap=cappedC.reduce((s,k)=>s+(AF[k].maxWetKg||99)*AF[k].dm,0); const lockedConcDm=Object.keys(AF).filter(k=>AF[k].isLocked&&AF[k].cat==='Concentrate').reduce((s,k)=>s+AF[k].forcedWet*AF[k].dm,0); const freeDmHR=Math.max(0,concNormDmCeil-lockedConcDm); const cScale=(sumCap>0&&freeDmHR(AF[k].maxWetKg||99)<=10?(AF[k].maxWetKg||99)*cScale:(AF[k].maxWetKg||99); for(let i=0;i<2000;i++){ const remTdn=tdnTargetG-tdnD,remCp=cpTargetG-cpD; if(remTdn<=tdnTargetG*0.01&&remCp<=cpTargetG*0.01)break; if(dmiD>=dmiMax*0.999)break; const cDm=Object.keys(schedule).reduce((s,k)=>FEED_MATRIX[k]?.cat==='Concentrate'?s+(schedule[k]||0)*FEED_MATRIX[k].dm:s,0); if(concNormDmCeil>0&&cDm>=concNormDmCeil*0.99)break; if(cpD>cpTargetG*1.30)break; if(tdnD>tdnTargetG*1.05)break; const cpCeil=cpD>cpTargetG*1.15; const eligPool=cpCeil?concPool.filter(k=>AF[k]).sort((a,b)=>AF[a].cp-AF[b].cp):concPool; const elig=eligPool.filter(k=>AF[k]&&(schedule[k]||0){ const f=AF[k];const s=(Math.max(0,remTdn)/tot*f.tdn*f.dm*1000+Math.max(0,remCp)/tot*f.cp*f.dm*1000)/f.cost; return s>b.score?{key:k,score:s}:b; },{key:null,score:-1}).key; if(!best)break; const micro=Math.min(0.04,(dmiMax-dmiD)*0.4,(effMax(best)-(schedule[best]||0))*AF[best].dm)/AF[best].dm; if(micro<=0||!add(best,micro))break; } if(tdnDcpTargetG*1.15){ const highCpConc=freeConc().filter(k=>AF[k].cp>0.25&&(schedule[k]||0)>0.1).sort((a,b)=>AF[b].cp-AF[a].cp); const lowCpConc=freeConc().filter(k=>AF[k].cp<=0.25).sort((a,b)=>AF[a].cost-AF[b].cost); for(const hk of highCpConc){ for(const lk of lowCpConc){ let go=true; while(go){ go=false; if((schedule[hk]||0)<0.11||tdnD>=tdnTargetG*0.99||cpD<=cpTargetG*1.05)break; const swapDm=0.1*AF[hk].dm,addWet=swapDm/AF[lk].dm; if((schedule[lk]||0)+addWet>effMax(lk)*1.001)break; const nT=tdnD-swapDm*AF[hk].tdn*1000+swapDm*AF[lk].tdn*1000; const nC=cpD-swapDm*AF[hk].cp*1000+swapDm*AF[lk].cp*1000; if(nC<=cpTargetG*1.40){ schedule[hk]=(schedule[hk]||0)-0.1;if(schedule[hk]<0.01)delete schedule[hk]; schedule[lk]=(schedule[lk]||0)+addWet;tdnD=nT;cpD=nC;go=true; } } } } const lowPool=freeConc().sort((a,b)=>AF[a].cp-AF[b].cp); for(const k of lowPool){ for(let i=0;i<500;i++){ if(tdnD>=tdnTargetG*0.99||dmiD>=dmiMax*0.999)break; if(cpD>cpTargetG*1.30)break; const cDm=Object.keys(schedule).reduce((s,j)=>FEED_MATRIX[j]?.cat==='Concentrate'?s+(schedule[j]||0)*FEED_MATRIX[j].dm:s,0); if(concNormDmCeil>0&&cDm>=concNormDmCeil*0.99)break; const micro=Math.min(0.04,(dmiMax-dmiD)*0.4,(effMax(k)-(schedule[k]||0))*AF[k].dm)/AF[k].dm; if(micro<=0||!add(k,micro))break; } if(tdnD>=tdnTargetG*0.99)break; } } if(dmiD0){ const k=fr[0],gap=dmiTarget-dmiD; const maxByTdn=tdnTargetG*1.10>tdnD?Math.max(0,(tdnTargetG*1.10-tdnD)/(AF[k].tdn*1000*AF[k].dm)):0; const allowed=Math.min(gap/AF[k].dm,(dmiMax-dmiD)/AF[k].dm,maxByTdn/AF[k].dm); if(allowed>0.1)add(k,allowed); } if(tdnDAF[a].cost-AF[b].cost); for(const k of tdnPool){ for(let i=0;i<600;i++){ if(tdnD>=tdnTargetG*0.99||cpD>=cpTargetG*1.30||dmiD>=dmiMax*0.999)break; const micro=Math.min(0.1,(dmiMax-dmiD)/AF[k].dm,(AF[k].maxWetKg||99)-(schedule[k]||0)); if(micro<=0||!add(k,micro))break; } if(tdnD>=tdnTargetG*0.99)break; } } if(dmiD>0.1&&(ndfD/dmiD)AF[b].ndf-AF[a].ndf); for(const k of hNdf){ const need=dmiD*NDF_MIN-ndfD;if(need<=0)break; const wet=Math.min(need/AF[k].ndf/AF[k].dm,(dmiMax-dmiD)/AF[k].dm); if(wet>0.05)add(k,wet); } } if(goals._costMinimise){ const baselineCaps=goals._baselineCaps||null; if(baselineCaps&&fr.length>0){ const capCats=new Set(['Green Cereal','Silage','Green Legume','Concentrate']); const cheapRK=fr[0]; for(const k of Object.keys(schedule)){ if(!AF[k]||!capCats.has(AF[k].cat)||AF[k].isLocked)continue; const baseCap=baselineCaps[k];if(baseCap===undefined)continue; const excess=(schedule[k]||0)-baseCap;if(excess<0.05)continue; const trimDm=excess*AF[k].dm; schedule[k]=baseCap;if(schedule[k]<0.01)delete schedule[k]; dmiD-=trimDm;tdnD-=trimDm*AF[k].tdn*1000;cpD-=trimDm*AF[k].cp*1000; ndfD-=trimDm*AF[k].ndf;caFeedD-=trimDm*AF[k].ca*1000;pFeedD-=trimDm*AF[k].p*1000; if(cheapRK){ const repWet=Math.min(trimDm/AF[cheapRK].dm,(dmiMax-dmiD)/AF[cheapRK].dm); if(repWet>0.01){ schedule[cheapRK]=(schedule[cheapRK]||0)+repWet; const repDm=repWet*AF[cheapRK].dm; dmiD+=repDm;tdnD+=repDm*AF[cheapRK].tdn*1000;cpD+=repDm*AF[cheapRK].cp*1000; ndfD+=repDm*AF[cheapRK].ndf;caFeedD+=repDm*AF[cheapRK].ca*1000;pFeedD+=repDm*AF[cheapRK].p*1000; } } } } const hmfTdnNow=Object.keys(schedule).filter(k=>AF[k]?.cat==='Hydroponic'&&AF[k].isLocked).reduce((s,k)=>(schedule[k]||0)*AF[k].dm*AF[k].tdn*1000+s,0); const hasLockedHmf=hmfTdnNow>0; if(!hasLockedHmf){ const TDN_FL=tdnTargetG*0.99,CP_FL=cpTargetG*0.99,cheapRK=fr[0]; if(tdnD>tdnTargetG*1.02){ const greenCands=Object.keys(schedule).filter(k=>!AF[k]?.isLocked&&(AF[k]?.cat==='Green Cereal'||AF[k]?.cat==='Silage')&&(schedule[k]||0)>0.11).sort((a,b)=>(AF[b]?.tdn||0)-(AF[a]?.tdn||0)); for(const k of greenCands){ let go=true; while(go){ go=false;const cur=schedule[k]||0;if(cur<0.11)break; const f=AF[k],tDm=0.1*f.dm,rf=cheapRK?AF[cheapRK]:null; const repDm=rf?tDm:0,repWet=rf?repDm/rf.dm:0; if(rf&&(dmiD-tDm+repDm)>dmiMax*1.001)break; const nT=tdnD-tDm*f.tdn*1000+(rf?repDm*rf.tdn*1000:0); const nC=cpD-tDm*f.cp*1000+(rf?repDm*rf.cp*1000:0); const nN=ndfD-tDm*f.ndf+(rf?repDm*rf.ndf:0); const nD=dmiD-tDm+repDm; if(nT>=TDN_FL&&nC>=cpTargetG*0.85&&nD>0){ schedule[k]=cur-0.1;if(schedule[k]<0.01)delete schedule[k]; dmiD=nD;tdnD=nT;cpD=nC;ndfD=nN; if(rf&&repWet>0){schedule[cheapRK]=(schedule[cheapRK]||0)+repWet;caFeedD+=repDm*rf.ca*1000-tDm*f.ca*1000;pFeedD+=repDm*rf.p*1000-tDm*f.p*1000;} else{caFeedD-=tDm*f.ca*1000;pFeedD-=tDm*f.p*1000;} go=true; } } if(tdnD<=tdnTargetG*1.02)break; } } if(tdnD>tdnTargetG*1.02||cpD>cpTargetG*1.30){ const concCands=Object.keys(schedule).filter(k=>!AF[k]?.isLocked&&AF[k]?.cat==='Concentrate'&&(schedule[k]||0)>0.11).sort((a,b)=>(AF[b]?.cost||0)-(AF[a]?.cost||0)); for(const k of concCands){ let go=true; while(go){ go=false;const cur=schedule[k]||0;if(cur<0.11)break; const f=AF[k],tDm=0.1*f.dm; const nT=tdnD-tDm*f.tdn*1000,nC=cpD-tDm*f.cp*1000,nD=dmiD-tDm,nN=ndfD-tDm*f.ndf; if(nT>=TDN_FL&&nC>=CP_FL&&nD>=dmiTarget*0.95){ schedule[k]=cur-0.1;if(schedule[k]<0.01)delete schedule[k]; dmiD=nD;tdnD=nT;cpD=nC;ndfD=nN;caFeedD-=tDm*f.ca*1000;pFeedD-=tDm*f.p*1000;go=true; } } } } if(dmiD0){ const k=fr[0],gap=dmiTarget-dmiD; const fw=Math.min(gap/AF[k].dm,(dmiMax-dmiD)/AF[k].dm); if(fw>0.05){ schedule[k]=(schedule[k]||0)+fw;const ad=fw*AF[k].dm; dmiD+=ad;tdnD+=ad*AF[k].tdn*1000;cpD+=ad*AF[k].cp*1000; ndfD+=ad*AF[k].ndf;caFeedD+=ad*AF[k].ca*1000;pFeedD+=ad*AF[k].p*1000; } } }else{ const TDN_FL=tdnTargetG*0.99,CP_FL=cpTargetG*0.99,cheapRK=fr[0]; const TRIM_CATS=new Set(['Concentrate','Hydroponic']); const trimCands=Object.keys(schedule).filter(k=>!AF[k]?.isLocked&&TRIM_CATS.has(AF[k]?.cat)&&(schedule[k]||0)>0.11).sort((a,b)=>(AF[b]?.cost||0)-(AF[a]?.cost||0)); for(const k of trimCands){ let go=true; while(go){ go=false;const cur=schedule[k]||0;if(cur<0.11)break; const f=AF[k],tDm=0.1*f.dm,rf=cheapRK?AF[cheapRK]:null; const repDm=rf?tDm:0,repWet=rf?repDm/rf.dm:0; if(rf&&(dmiD-tDm+repDm)>dmiMax*1.001)break; const nT=tdnD-tDm*f.tdn*1000+(rf?repDm*rf.tdn*1000:0); const nC=cpD-tDm*f.cp*1000+(rf?repDm*rf.cp*1000:0); const nN=ndfD-tDm*f.ndf+(rf?repDm*rf.ndf:0); const nD=dmiD-tDm+repDm; const ndfOk=nD<0.1||(nN/nD)>=NDF_MIN; if(nT>=TDN_FL&&nC>=CP_FL&&ndfOk&&nD>0){ schedule[k]=cur-0.1;if(schedule[k]<0.01)delete schedule[k]; dmiD=nD;tdnD=nT;cpD=nC;ndfD=nN; if(rf&&repWet>0){schedule[cheapRK]=(schedule[cheapRK]||0)+repWet;caFeedD+=repDm*rf.ca*1000-tDm*f.ca*1000;pFeedD+=repDm*rf.p*1000-tDm*f.p*1000;} else{caFeedD-=tDm*f.ca*1000;pFeedD-=tDm*f.p*1000;} go=true; } } } if((tdnDAF[a].cost-AF[b].cost); for(const k of rec){ for(let i=0;i<500;i++){ if(tdnD>=tdnTargetG*0.99&&cpD>=cpTargetG*0.99)break; if(dmiD>=dmiMax*0.999)break; const m=Math.min(0.1,(dmiMax-dmiD)/AF[k].dm,(AF[k].maxWetKg||99)-(schedule[k]||0)); if(m<=0||!add(k,m))break; } if(tdnD>=tdnTargetG*0.99&&cpD>=cpTargetG*0.99)break; } } } } return{schedule,dmiD,tdnD,cpD,ndfD,caFeedD,pFeedD}; } // ─── MODALS ────────────────────────────────────────────────────────────────── function openModal(id){document.getElementById(id).classList.add('open');} function closeModal(id){document.getElementById(id).classList.remove('open');} // modal overlay click-to-close document.querySelectorAll('.modal-overlay').forEach(el=>el.addEventListener('click',function(e){if(e.target===this)closeModal(this.id);})); function buildSources(){ document.getElementById('sourcesList').innerHTML=SOURCES.map(s=>`
${s.title}
${s.desc}
${s.pdfUrl?`PDF`:``}
`).join(''); } function openFeedInfo(feedKey){ const f=FEEDS.find(x=>x.key===feedKey); document.getElementById('infoModalTitle').textContent=f.name; document.getElementById('infoNutrientChips').innerHTML=[ `DM ${(f.dm*100).toFixed(0)}%`, `CP ${f.cp.toFixed(1)}%`, `TDN ${f.tdn.toFixed(1)}%`, `NDF ${f.ndf.toFixed(1)}%`, `Ca ${f.ca.toFixed(2)}%`, `P ${f.p.toFixed(2)}%`, `₹${f.cost}/kg`, ].join(''); document.getElementById('infoDesc').textContent=f.desc; const ph=document.getElementById('infoImgPlaceholder'); ph.innerHTML=feedImages[feedKey]?``:`No image available`; openModal('infoModal'); } function expandFeedImage(el){ const img=el.querySelector('img');if(!img)return; const ov=document.createElement('div'); ov.style.cssText='position:fixed;inset:0;background:rgba(0,0,0,0.85);z-index:99999;display:flex;align-items:center;justify-content:center;cursor:zoom-out'; const bi=document.createElement('img');bi.src=img.src; bi.style.cssText='max-width:92vw;max-height:88vh;border-radius:8px'; ov.appendChild(bi);ov.addEventListener('click',()=>ov.remove());document.body.appendChild(ov); } // ─── STEP FLOW ─────────────────────────────────────────────────────────────── function toggleStep(id){document.getElementById(id).classList.toggle('collapsed');} function openStep(id){ const el=document.getElementById(id);el.classList.remove('hidden','collapsed'); setTimeout(()=>{try{el.scrollIntoView({behavior:'smooth',block:'start'});}catch(e){}},100); } function collapseStep(id){document.getElementById(id).classList.add('collapsed');} function onLifeStageChange(){ const ls=document.getElementById('lifeStage').value; document.getElementById('milkField').style.display=ls==='lactating'?'':'none'; document.getElementById('fatField').style.display=ls==='lactating'?'':'none'; } document.getElementById('animalType').addEventListener('change',function(){ const fi=document.getElementById('fatPct');if(!fi)return; if(this.value==='cow_indigenous')fi.value=4.5; else if(this.value==='cow_crossbred')fi.value=3.5; updateBreedNote(); }); function updateBreedNote(){ const el=document.getElementById('breedNote');if(!el)return; const v=document.getElementById('animalType').value; el.textContent=v==='cow_indigenous' ?'DMI adjusted downward (BW×2.2% + milk×2.5%) — reflects lower voluntary intake of Gir / Kankrej / Sahiwal. Same TDN, CP, Ca, P coefficients as crossbred per ICAR 2013.' :v==='buffalo' ?'DMI: BW×2.5% + milk×2.5% · Buffalo-specific TDN and Ca/P coefficients' :'DMI: BW×2.5% + milk×2.5% · Nutrient coefficients: ICAR 2013 standard'; } function confirmAnimalDetails(){ goalsState=computeGoals(); document.getElementById('step1Num').classList.add('done'); collapseStep('step1Section');renderStep2();openStep('step2Section'); } function renderStep2(){ const g=goalsState; document.getElementById('goalsRow').innerHTML=[ {label:'DMI',val:g.dmiTarget.toFixed(1),unit:'kg/day'}, {label:'TDN',val:(g.tdnTargetG/1000).toFixed(2),unit:'kg/day'}, {label:'CP',val:g.cpTargetG.toFixed(0),unit:'g/day'}, {label:'NDF',val:'≥28%',unit:'of DM'}, {label:'Ca',val:g.caTargetG.toFixed(1),unit:'g/day'}, {label:'P',val:g.pTargetG.toFixed(1),unit:'g/day'}, ].map(d=>`
${d.label}
${d.val}
${d.unit}
`).join(''); FEEDS.forEach(f=>{if(baselineChecked[f.key]===undefined)baselineChecked[f.key]=true;}); } function openStep22(){ document.getElementById('step2Num').classList.add('done'); collapseStep('step2Section');renderFeedSelectList();openStep('step22Section'); } function renderFeedSelectList(){ const bc={'Roughage':'cat-roughage','Green Cereal':'cat-green','Green Legume':'cat-green','Silage':'cat-silage','Hydroponic':'cat-hydro','Concentrate':'cat-conc'}; let html=''; CAT_GROUPS.forEach(grp=>{ const feeds=FEEDS.filter(f=>grp.cats.includes(f.cat));if(!feeds.length)return; html+=`
${grp.label}
`; feeds.forEach(f=>{ const checked=baselineChecked[f.key]!==false; html+=`
${f.cat}
`; }); html+=`
`; }); document.getElementById('feedSelectList').innerHTML=html; } function toggleFeedCheck(key,val){ baselineChecked[key]=val; const row=document.getElementById('feedrow_'+key); if(row){val?row.classList.add('checked'):row.classList.remove('checked');} } function calculateBaseline(){ const checked=FEEDS.filter(f=>baselineChecked[f.key]!==false).map(f=>f.key); const res=runSolverCore({...goalsState,_costMinimise:true},{},checked); const asmm=computeAsmm(res.caFeedD,res.pFeedD,goalsState.caTargetG,goalsState.pTargetG,goalsState.bw); baselineResult={...res,asmm,goals:goalsState,checked}; document.getElementById('step22Num').classList.add('done'); collapseStep('step22Section');renderDietSection('baseline',res,asmm,goalsState);openStep('step3Section'); } function calcRationCost(schedule,dose_g){ let cost=0; FEEDS.forEach(f=>{cost+=(schedule[f.key]||0)*(customFeedCosts[f.key]||f.cost);}); cost+=dose_g*(customFeedCosts['asmm']||ASMM.cost_per_g); return cost; } function buildAlerts(res,asmm,goals){ const alerts=[]; const ndfPct=res.dmiD>0?res.ndfD/res.dmiD*100:0; const totalCaG=res.caFeedD+asmm.ca,totalPG=res.pFeedD+asmm.p; const concDmPct=(()=>{ const cd=Object.keys(res.schedule).reduce((s,k)=>{ const cat=FEED_MATRIX[k]?.cat,dm=(res.schedule[k]||0)*FEED_MATRIX[k].dm; return cat==='Concentrate'?s+dm:s; },0); return res.dmiD>0?cd/res.dmiD*100:0; })(); const milk=goals.milk||0,ls=goals.lifeStage; const isNonLac=(ls==='pregnant_t1'||ls==='pregnant_t2'||ls==='pregnant_t3'||ls==='dry'||ls==='calf'); const saraLim=isNonLac?30:milk<=5?40:milk<=10?50:milk<=15?60:70; const hasAnyGreen=Object.keys(res.schedule).some(k=>(res.schedule[k]||0)>0.1&&['Green Cereal','Silage','Green Legume','Hydroponic'].includes(FEED_MATRIX[k]?.cat)); if(!hasAnyGreen&&goals.lifeStage==='lactating')alerts.push(`
No Green Fodder in DietAdd maize fodder, jowar, napier, silage, berseem, or HMF to meet energy targets within SARA-safe limits.
`); if(res.dmiD>goals.dmiMax*1.01)alerts.push(`
Stomach Overfill RiskDMI ${res.dmiD.toFixed(2)} kg exceeds max ${goals.dmiMax.toFixed(2)} kg.
`); if(concDmPct>saraLim)alerts.push(`
Rumen Acidity Risk (SARA)Concentrate DM ${concDmPct.toFixed(1)}% exceeds safe limit ${saraLim}% for current yield.
`); if(ndfPct<28)alerts.push(`
Low Fibre (NDF)NDF ${ndfPct.toFixed(1)}% is below recommended 28%. Add more roughage.
`); if(res.tdnD=goals.dmiMax*0.98)alerts.push(`
Energy Gap — Feed Quality ConstraintTDN target cannot be met within stomach capacity. Consider higher-energy feeds.
`); if(totalCaG
Calcium ShortfallTotal Ca ${totalCaG.toFixed(1)}g below 90% of target ${goals.caTargetG.toFixed(1)}g.
`); if(totalPG
Phosphorus ShortfallTotal P ${totalPG.toFixed(1)}g below 90% of target ${goals.pTargetG.toFixed(1)}g.
`); if(res.tdnD
Energy ShortfallTDN ${(res.tdnD/1000).toFixed(2)}kg vs target ${(goals.tdnTargetG/1000).toFixed(2)}kg.
`); if(res.cpD>goals.cpTargetG*1.35)alerts.push(`
Excess Protein WarningCP ${res.cpD.toFixed(0)}g exceeds 135% of target. Risk of liver urea strain and wasted feed cost.
`); if(res.cpD
Protein ShortfallCP ${res.cpD.toFixed(0)}g vs target ${goals.cpTargetG.toFixed(0)}g. Add mustard/cottonseed cake or commercial feed.
`); if(res.dmiD
Low Dry Matter IntakeDMI ${res.dmiD.toFixed(2)}kg below target ${goals.dmiTarget.toFixed(2)}kg.
`); if(!['alert-error','alert-warn'].some(c=>alerts.some(a=>a.includes(c))))alerts.push(`
All Nutrient Targets MetThis ration meets ICAR 2013 / NDDB RBP recommendations.
`); return alerts; } function renderDietSection(which,res,asmm,goals){ const rounded={}; FEEDS.forEach(f=>{ const wet=res.schedule[f.key]||0; if(wet<0.01){rounded[f.key]=0;return;} rounded[f.key]=Math.round(wet*10)/10; }); let dmiCheck=FEEDS.reduce((s,f)=>{const w=rounded[f.key]||0;return s+w*(FEED_MATRIX[f.key]?.dm||0.9);},0); if(dmiCheck>goals.dmiMax*1.001){ const toTrim=FEEDS.filter(f=>rounded[f.key]>0&&FEED_MATRIX[f.key]?.cat==='Roughage').sort((a,b)=>(rounded[b.key]||0)-(rounded[a.key]||0)); for(const f of toTrim){ while(dmiCheck>goals.dmiMax*1.001&&rounded[f.key]>=0.1){ rounded[f.key]=Math.round((rounded[f.key]-0.1)*10)/10; dmiCheck=FEEDS.reduce((s,ff)=>{const w=rounded[ff.key]||0;return s+w*(FEED_MATRIX[ff.key]?.dm||0.9);},0); } if(dmiCheck<=goals.dmiMax*1.001)break; } } let dmiD=0,tdnD=0,cpD=0,ndfD=0,caFeedD=0,pFeedD=0; FEEDS.forEach(f=>{ const wet=rounded[f.key]||0;if(!wet)return; const fm=FEED_MATRIX[f.key],dm=wet*fm.dm; dmiD+=dm;tdnD+=dm*fm.tdn*1000;cpD+=dm*fm.cp*1000; ndfD+=dm*fm.ndf;caFeedD+=dm*fm.ca*1000;pFeedD+=dm*fm.p*1000; }); if(tdnDv)):{}; const topUpCandidates=['commercial_feed','cottonseed_cake','mustard_cake'] .filter(k=>FEED_MATRIX[k]&&!lockedInDisplay[k]&&(rounded[k]||0)<(FEED_MATRIX[k].maxWetKg||99)) .sort((a,b)=>FEED_MATRIX[a].cost-FEED_MATRIX[b].cost); for(const k of topUpCandidates){ while(tdnDgoals.dmiMax)break; rounded[k]=(rounded[k]||0)+0.1; dmiD+=addDm;tdnD+=addDm*fm.tdn*1000;cpD+=addDm*fm.cp*1000; ndfD+=addDm*fm.ndf;caFeedD+=addDm*fm.ca*1000;pFeedD+=addDm*fm.p*1000; } if(tdnD>=goals.tdnTargetG*0.97)break; } } res={...res,schedule:rounded,dmiD,tdnD,cpD,ndfD,caFeedD,pFeedD}; asmm=computeAsmm(caFeedD,pFeedD,goals.caTargetG,goals.pTargetG,goals.bw); if(which==='custom'&&baselineResult){ const locked2={};FEEDS.forEach(f=>{if(customLockMap[f.key])locked2[f.key]=customQtyMap[f.key];}); if((locked2['hmf']||0)>0||(res.schedule['hmf']||0)>0){ const bRW=FEEDS.filter(f=>f.cat==='Roughage').reduce((s,f)=>s+(baselineResult.schedule[f.key]||0),0); const cRW=FEEDS.filter(f=>f.cat==='Roughage').reduce((s,f)=>s+(rounded[f.key]||0),0); if(cRWf.cat==='Roughage'&&!locked2[f.key]).sort((a,b)=>a.cost-b.cost)[0]?.key; if(cheapKey){ const roomDm=Math.max(0,goals.dmiMax-dmiD); const addWet=Math.min(Math.ceil(deficit*10)/10,Math.floor(roomDm/FEED_MATRIX[cheapKey].dm*10)/10); if(addWet>=0.1){ rounded[cheapKey]=(rounded[cheapKey]||0)+addWet; const ad=addWet*FEED_MATRIX[cheapKey].dm; dmiD+=ad;tdnD+=ad*FEED_MATRIX[cheapKey].tdn*1000;cpD+=ad*FEED_MATRIX[cheapKey].cp*1000; ndfD+=ad*FEED_MATRIX[cheapKey].ndf;caFeedD+=ad*FEED_MATRIX[cheapKey].ca*1000;pFeedD+=ad*FEED_MATRIX[cheapKey].p*1000; res={...res,schedule:rounded,dmiD,tdnD,cpD,ndfD,caFeedD,pFeedD}; asmm=computeAsmm(caFeedD,pFeedD,goals.caTargetG,goals.pTargetG,goals.bw); } } } } } const pfx=which==='baseline'?'baseline':'custom'; const totalCaG=res.caFeedD+asmm.ca,totalPG=res.pFeedD+asmm.p; const ndfPct=res.dmiD>0?(res.ndfD/res.dmiD*100):0; const kpis=[ {label:'DMI',actual:res.dmiD.toFixed(2),target:goals.dmiMax.toFixed(2),unit:'kg',sublabel:`min ${goals.dmiTarget.toFixed(1)}`,pct:res.dmiD/goals.dmiMax}, {label:'TDN',actual:(res.tdnD/1000).toFixed(1),target:(goals.tdnTargetG/1000).toFixed(1),unit:'kg',pct:res.tdnD/1000/(goals.tdnTargetG/1000)}, {label:'CP',actual:res.cpD.toFixed(0),target:goals.cpTargetG.toFixed(0),unit:'g',pct:res.cpD/goals.cpTargetG}, {label:'NDF',actual:ndfPct.toFixed(1)+'%',target:'≥28%',unit:'',pct:ndfPct/28}, {label:asmm.dose_g>0?`Ca +ASMM ${asmm.dose_g}g`:'Ca (feed only)',actual:totalCaG.toFixed(1),target:goals.caTargetG.toFixed(1),unit:'g',pct:totalCaG/goals.caTargetG}, {label:asmm.dose_g>0?`P +ASMM ${asmm.dose_g}g`:'P (feed only)',actual:totalPG.toFixed(1),target:goals.pTargetG.toFixed(1),unit:'g',pct:totalPG/goals.pTargetG}, ]; document.getElementById(pfx+'Kpis').innerHTML=`
${kpis.map(k=>{ const pct=Math.min(k.pct,2),cls=k.pct>=1?'prog-ok':k.pct>=0.9?'prog-warn':'prog-under'; const tl=k.sublabel?`cap ${k.target}${k.unit} · ${k.sublabel}${k.unit}`:`target ${k.target}${k.unit}`; return `
${k.label}
${k.actual}${k.unit}${tl}
`; }).join('')}
`; const alerts=buildAlerts(res,asmm,goals); document.getElementById(pfx+'Alerts').innerHTML=alerts.length?`
${alerts.join('')}
`:''; const activeRows=FEEDS.filter(f=>(res.schedule[f.key]||0)>0.01); document.getElementById(pfx+'RationWrap').innerHTML=`
${activeRows.map(f=>{const wet=res.schedule[f.key];const dm=wet*(FEED_MATRIX[f.key]?.dm||f.dm);return``;}).join('')} ${asmm.dose_g>0?``:''}
FeedWet kgDM kgTDN gCP gCa gP gCost ₹
${f.name}${wet.toFixed(1)}${dm.toFixed(3)}${(dm*f.tdn*10).toFixed(0)}${(dm*f.cp*10).toFixed(0)}${(dm*f.ca*10).toFixed(2)}${(dm*f.p*10).toFixed(2)}${(wet*(customFeedCosts[f.key]||f.cost)).toFixed(1)}
ASMM (Mineral Mix)${(asmm.dose_g/1000).toFixed(3)}${asmm.ca.toFixed(2)}${asmm.p.toFixed(2)}${(asmm.dose_g*(customFeedCosts['asmm']||ASMM.cost_per_g)).toFixed(1)}
Total${activeRows.reduce((s,f)=>s+(res.schedule[f.key]||0),0).toFixed(1)}${res.dmiD.toFixed(3)}${res.tdnD.toFixed(0)}${res.cpD.toFixed(0)}${totalCaG.toFixed(2)}${totalPG.toFixed(2)}${calcRationCost(res.schedule,asmm.dose_g).toFixed(1)}
`; document.getElementById(pfx+'AsmmBox').innerHTML=asmm.dose_g>0 ?`
Mineral Supplement (ASMM)
Feed Ca: ${res.caFeedD.toFixed(1)}g | Feed P: ${res.pFeedD.toFixed(1)}g | Targets — Ca: ${goals.caTargetG.toFixed(1)}g, P: ${goals.pTargetG.toFixed(1)}g
Recommended ASMM: ${asmm.dose_g}g/day (driven by ${asmm.caDriver} deficit) → Total Ca: ${totalCaG.toFixed(1)}g, P: ${totalPG.toFixed(2)}g${totalCaG` :`
No Mineral Supplement Needed
Feed-sourced Ca: ${res.caFeedD.toFixed(1)}g (target ${goals.caTargetG.toFixed(1)}g) | P: ${res.pFeedD.toFixed(1)}g (target ${goals.pTargetG.toFixed(1)}g)
Both Ca and P fully met by feed alone — no ASMM required today.
`; if(which==='baseline')baselineResult={...baselineResult,schedule:rounded,dmiD,tdnD,cpD,ndfD,caFeedD,pFeedD,asmm}; if(which==='custom')customResult={...customResult,schedule:rounded,dmiD,tdnD,cpD,ndfD,caFeedD,pFeedD,asmm}; } // ─── CUSTOMISE ─────────────────────────────────────────────────────────────── function openCustomize(){collapseStep('step3Section');renderCustomizeList();openStep('step4Section');} function renderCustomizeList(){ const res=baselineResult; const bc={'Roughage':'cat-roughage','Green Cereal':'cat-green','Green Legume':'cat-green','Silage':'cat-silage','Hydroponic':'cat-hydro','Concentrate':'cat-conc'}; let html=''; CAT_GROUPS.forEach(grp=>{ const feeds=FEEDS.filter(f=>grp.cats.includes(f.cat)&&baselineChecked[f.key]!==false);if(!feeds.length)return; html+=`
${grp.label}
`; feeds.forEach(f=>{ const suggested=res.schedule[f.key]||0; const qty=customQtyMap[f.key]!==undefined?customQtyMap[f.key]:suggested; const locked=customLockMap[f.key]||false; html+=`
0?'checked':''} onchange="toggleCustCheck('${f.key}',this.checked)" id="custchk_${f.key}">
`; }); html+=`
`; }); document.getElementById('customizeFeedList').innerHTML=html; const hmfInBaseline=(baselineResult.schedule['hmf']||0)>0.1; document.getElementById('hmfPromptWrap').classList[hmfInBaseline?'add':'remove']('hidden'); if(hmfInBaseline)document.getElementById('hmfCheck').checked=true; } function toggleCustCheck(key,val){ const row=document.getElementById('custrow_'+key); if(row){val?row.classList.add('checked'):row.classList.remove('checked');} if(!val){customQtyMap[key]=0;document.getElementById('custqty_'+key).value='0.0';} else if(!customQtyMap[key]||customQtyMap[key]===0){ customQtyMap[key]=Math.round((baselineResult.schedule[key]||0.5)*10)/10; document.getElementById('custqty_'+key).value=customQtyMap[key].toFixed(1); } } function onCustQtyChange(key){ let val=Math.round((parseFloat(document.getElementById('custqty_'+key).value)||0)*10)/10; customQtyMap[key]=val;document.getElementById('custqty_'+key).value=val.toFixed(1); const row=document.getElementById('custrow_'+key); if(row){val>0?row.classList.add('checked'):row.classList.remove('checked');} } function toggleLock(key){ customLockMap[key]=!customLockMap[key]; const btn=document.getElementById('lockbtn_'+key),inp=document.getElementById('custqty_'+key); if(customLockMap[key]){ btn.classList.add('active');btn.textContent=' Locked';inp.classList.add('locked'); const snapped=Math.round((parseFloat(inp.value)||0)*10)/10; customQtyMap[key]=snapped;inp.value=snapped.toFixed(1); }else{btn.classList.remove('active');btn.textContent='Lock';inp.classList.remove('locked');} } function enforceRoughageFloor(cR,baseRes,lockedFeeds,dmiMaxG){ if(!baseRes||(lockedFeeds['hmf']||0)<=0)return; const baseRoughWet=FEEDS.filter(f=>f.cat==='Roughage').reduce((s,f)=>s+(baseRes.schedule[f.key]||0),0); const custRoughWet=FEEDS.filter(f=>f.cat==='Roughage').reduce((s,f)=>s+(cR.schedule[f.key]||0),0); if(custRoughWetf.cat==='Roughage'&&!lockedFeeds[f.key]).sort((a,b)=>a.cost-b.cost)[0]?.key; if(cheapKey){ const roomDm=dmiMaxG?Math.max(0,dmiMaxG-cR.dmiD):999; const addWet=Math.min(deficit,roomDm/FEED_MATRIX[cheapKey].dm); if(addWet>0.01){ cR.schedule[cheapKey]=(cR.schedule[cheapKey]||0)+addWet; const addDm=addWet*FEED_MATRIX[cheapKey].dm; cR.dmiD+=addDm;cR.tdnD+=addDm*FEED_MATRIX[cheapKey].tdn*1000; cR.cpD+=addDm*FEED_MATRIX[cheapKey].cp*1000;cR.ndfD+=addDm*FEED_MATRIX[cheapKey].ndf; cR.caFeedD+=addDm*FEED_MATRIX[cheapKey].ca*1000;cR.pFeedD+=addDm*FEED_MATRIX[cheapKey].p*1000; } } } } function recalculateCustom(){ const locked={}; FEEDS.forEach(f=>{ const raw=parseFloat(document.getElementById('custqty_'+f.key)?.value)||0; const val=Math.round(raw*10)/10; customQtyMap[f.key]=val; if(customLockMap[f.key])locked[f.key]=val; }); const hmfBox=document.getElementById('hmfCheck'); if(hmfBox&&hmfBox.checked&&locked['hmf']===undefined){ const hq=Math.round((parseFloat(document.getElementById('hmfQtyInput')?.value)||5)*10)/10; locked['hmf']=hq;customQtyMap['hmf']=hq; } const checkedSet=new Set(FEEDS.filter(f=>baselineChecked[f.key]!==false).map(f=>f.key)); Object.keys(locked).forEach(k=>checkedSet.add(k)); const res=runSolverCore({...goalsState,_costMinimise:true,_baselineCaps:baselineResult?baselineResult.schedule:null},locked,Array.from(checkedSet)); enforceRoughageFloor(res,baselineResult,locked,goalsState.dmiMax); const asmm=computeAsmm(res.caFeedD,res.pFeedD,goalsState.caTargetG,goalsState.pTargetG,goalsState.bw); customResult={...res,asmm,goals:goalsState}; document.getElementById('step4Num').classList.add('done'); collapseStep('step4Section');renderDietSection('custom',res,asmm,goalsState);openStep('step5Section'); document.getElementById('exportSection').classList.remove('hidden'); } function getMilkPrice(){ if(milkPricePerLitre)return milkPricePerLitre; const fat=goalsState.fat||4.0; if(goalsState.animalType==='buffalo')return(fat/100)*875*1.03; const snf=8.5,eqFat=fat+(snf*(2/3)); return(eqFat/100)*388.65*1.03; } function openEconomics(which){ if(which==='baseline'){document.getElementById('econBaselineContent').innerHTML=renderEconContent('baseline',null);openStep('econBaselineSection');} else{document.getElementById('econCustomContent').innerHTML=renderEconContent('custom','baseline');openStep('econCustomSection');} document.getElementById('exportSection').classList.remove('hidden'); } function renderEconContent(which,compareWith){ const r=which==='baseline'?baselineResult:customResult;if(!r)return '

No data yet.

'; const ls=goalsState.lifeStage,isLac=ls==='lactating',milk=goalsState.milk||0,bw=goalsState.bw||450; const mp=getMilkPrice(),dailyInc=milk*mp; const cost=calcRationCost(r.schedule,r.asmm.dose_g); const cpl=milk>0?cost/milk:0,costPct=dailyInc>0?cost/dailyInc*100:0; let breakdown=''; FEEDS.forEach(f=>{const wet=r.schedule[f.key]||0;if(wet<0.01)return;breakdown+=`
${f.name} (${wet.toFixed(1)} kg)₹${(wet*(customFeedCosts[f.key]||f.cost)).toFixed(1)}
`;}); if(r.asmm.dose_g>0)breakdown+=`
ASMM (${r.asmm.dose_g}g)₹${(r.asmm.dose_g*ASMM.cost_per_g).toFixed(1)}
`; let costInputs=`
Edit Feed Costs (₹/kg)
`; FEEDS.forEach(f=>{const wet=r.schedule[f.key]||0;if(wet<0.01)return;costInputs+=`
`;}); if(isLac)costInputs+=`
`; costInputs+=`
`; const dirty=window['costsDirty_'+which]||false; const reoptBtn=dirty?`
Costs changed — diet not yet re-optimised
Re-run to get cheapest balanced diet at new costs.
`:''; let summaryMetrics=''; if(isLac){ const bCls=costPct<80?'econ-banner good':'econ-banner warn'; const bMsg=costPct<80?` Feed cost is ${costPct.toFixed(1)}% of daily milk income — within healthy range.`:` Feed cost is ${costPct.toFixed(1)}% of daily milk income — above 80% threshold.`; summaryMetrics=`
Total feed cost/day₹${cost.toFixed(1)}
Milk income/day₹${dailyInc.toFixed(1)}
Cost per litre milk₹${cpl.toFixed(2)}
Cost as % of income${costPct.toFixed(1)}%
${bMsg}
`; }else{ const stageLabel=ls==='calf'?'Heifer / Calf':ls==='dry'?'Dry Cow':ls==='pregnant_t1'?'Pregnant T1':ls==='pregnant_t2'?'Pregnant T2':'Pregnant T3'; summaryMetrics=`
Total feed cost/day₹${cost.toFixed(1)}
Cost per kg body weight₹${(cost/bw).toFixed(2)}
Monthly feed cost (est.)₹${(cost*30).toFixed(0)}
Daily maintenance cost for ${stageLabel}: ₹${cost.toFixed(1)}/day
`; } let compareHtml=''; if(compareWith==='baseline'&&baselineResult){ const costB=calcRationCost(baselineResult.schedule,baselineResult.asmm.dose_g); const savDay=costB-cost,savMo=savDay*30; const savCol=savMo>=0?'var(--green-dark)':'var(--red)'; const savSign=savMo>=0?'+':'-'; let cardRows=''; if(isLac){ const cplB=milk>0?costB/milk:0,pctB=dailyInc>0?costB/dailyInc*100:0; cardRows=`
Baseline Diet
Daily feed cost₹${costB.toFixed(1)}
Cost per litre₹${cplB.toFixed(2)}
% of milk income${pctB.toFixed(1)}%
Customised Diet
Daily feed cost₹${cost.toFixed(1)}
Cost per litre₹${cpl.toFixed(2)}
% of milk income${costPct.toFixed(1)}%
`; }else{ cardRows=`
Baseline Diet
Daily feed cost₹${costB.toFixed(1)}
Monthly cost₹${(costB*30).toFixed(0)}
Customised Diet
Daily feed cost₹${cost.toFixed(1)}
Monthly cost₹${(cost*30).toFixed(0)}
`; } if(isLac&&savDay<0){ compareHtml=`
Note on customised diet cost
There is some additional monthly cost with customised diet having HMF. The slightly extra amount provides year-round fodder availability, better digestibility and improved Ca:P balance.
`; }else{ compareHtml=`
${cardRows}
${savMo>=0?'Monthly savings with customised diet':'Additional monthly cost with customised diet'}
${savSign}₹${Math.abs(savMo).toFixed(0)}/month
`; } } return `
${reoptBtn}${breakdown}
${summaryMetrics}${compareHtml}
${costInputs}
`; } function markCostsDirty(which){window['costsDirty_'+which]=true;updateEcon();} function updateEcon(){ if(!document.getElementById('econBaselineSection').classList.contains('hidden'))document.getElementById('econBaselineContent').innerHTML=renderEconContent('baseline',null); if(!document.getElementById('econCustomSection').classList.contains('hidden'))document.getElementById('econCustomContent').innerHTML=renderEconContent('custom','baseline'); } function reoptimiseDiet(which){ if(which==='baseline'&&goalsState&&baselineResult){ const checked=baselineResult.checked||FEEDS.filter(f=>baselineChecked[f.key]!==false).map(f=>f.key); const res=runSolverCore({...goalsState,_costMinimise:true},{},checked); const asmm=computeAsmm(res.caFeedD,res.pFeedD,goalsState.caTargetG,goalsState.pTargetG,goalsState.bw); baselineResult={...res,asmm,goals:goalsState,checked}; renderDietSection('baseline',res,asmm,goalsState); if(customResult){ const cL={};Object.entries(customQtyMap).forEach(([k,v])=>{if(customLockMap[k])cL[k]=v;}); const custFeeds=[...new Set([...checked,...Object.keys(cL)])]; const cR=runSolverCore({...goalsState,_costMinimise:true,_baselineCaps:res.schedule},cL,custFeeds); enforceRoughageFloor(cR,{...res},cL,goalsState.dmiMax); const cA=computeAsmm(cR.caFeedD,cR.pFeedD,goalsState.caTargetG,goalsState.pTargetG,goalsState.bw); customResult={...cR,asmm:cA,goals:goalsState};renderDietSection('custom',cR,cA,goalsState); } window['costsDirty_baseline']=false;window['costsDirty_custom']=false;updateEcon(); setTimeout(()=>{try{document.getElementById('step3Section').scrollIntoView({behavior:'smooth',block:'start'});}catch(e){}},150); }else if(which==='custom'&&goalsState&&customResult){ const cL={};Object.entries(customQtyMap).forEach(([k,v])=>{if(customLockMap[k])cL[k]=v;}); const chk=FEEDS.filter(f=>baselineChecked[f.key]!==false).map(f=>f.key); const bR=runSolverCore({...goalsState,_costMinimise:true},{},chk); const bA=computeAsmm(bR.caFeedD,bR.pFeedD,goalsState.caTargetG,goalsState.pTargetG,goalsState.bw); baselineResult={...bR,asmm:bA,goals:goalsState,checked:chk}; const custFeeds2=[...new Set([...chk,...Object.keys(cL)])]; const cR=runSolverCore({...goalsState,_costMinimise:true,_baselineCaps:bR.schedule},cL,custFeeds2); enforceRoughageFloor(cR,bR,cL,goalsState.dmiMax); const cA=computeAsmm(cR.caFeedD,cR.pFeedD,goalsState.caTargetG,goalsState.pTargetG,goalsState.bw); customResult={...cR,asmm:cA,goals:goalsState};renderDietSection('custom',cR,cA,goalsState); renderDietSection('baseline',bR,bA,goalsState); window['costsDirty_baseline']=false;window['costsDirty_custom']=false;updateEcon(); setTimeout(()=>{try{document.getElementById('step5Section').scrollIntoView({behavior:'smooth',block:'start'});}catch(e){}},150); } } function exportPDF(){ if(!goalsState){showToast('Please calculate a diet first.');return;} const g=goalsState, milk=g.milk||0, bw=g.bw||450, isLac=g.lifeStage==='lactating'; const mp=getMilkPrice(); const aLabel=g.animalType==='cow_crossbred'?'Crossbred Cow':g.animalType==='cow_indigenous'?'Indigenous Cow':'Buffalo'; const date=new Date().toLocaleDateString('en-IN',{day:'2-digit',month:'long',year:'numeric'}); const logo='https://www.shunya.live/wp-content/uploads/2026/02/Sunya_Logo_highres-1-scaled.png'; function esc(s){return String(s).replace(/&/g,'&').replace(//g,'>');} function rationRows(res){ if(!res)return ''; const ar=FEEDS.filter(function(f){return(res.schedule[f.key]||0)>0.01;}); let rows=ar.map(function(f){ const wet=res.schedule[f.key]; const dm=wet*(FEED_MATRIX[f.key]?FEED_MATRIX[f.key].dm:f.dm); return ''+esc(f.name)+''+wet.toFixed(1)+''+dm.toFixed(3)+''+(dm*f.tdn*10).toFixed(0)+''+(dm*f.cp*10).toFixed(0)+'₹'+(wet*(customFeedCosts[f.key]||f.cost)).toFixed(1)+''; }).join(''); if(res.asmm&&res.asmm.dose_g>0){ rows+='ASMM '+res.asmm.dose_g+'g/day'+(res.asmm.dose_g/1000).toFixed(3)+'———₹'+(res.asmm.dose_g*ASMM.cost_per_g).toFixed(1)+''; } const tc=calcRationCost(res.schedule,res.asmm?res.asmm.dose_g:0); const totalWet=ar.reduce(function(s,f){return s+(res.schedule[f.key]||0);},0); rows+='TOTAL'+totalWet.toFixed(1)+''+res.dmiD.toFixed(3)+''+res.tdnD.toFixed(0)+''+res.cpD.toFixed(0)+'₹'+tc.toFixed(1)+(isLac&&milk>0?' (₹'+(tc/milk).toFixed(2)+'/L)':'')+''; return rows; } let savingsHtml=''; if(baselineResult&&customResult){ const cB=calcRationCost(baselineResult.schedule,baselineResult.asmm.dose_g); const cC=calcRationCost(customResult.schedule,customResult.asmm.dose_g); const sD=cB-cC, sM=sD*30, sY=sD*365, sg=sD>=0?'+':'-'; const col=sD>=0?'#166534':'#991b1b'; savingsHtml='
' +'
Savings Summary
' +'' +'' +(isLac&&milk>0?'':'') +'' +'' +'
MetricBaselineCustomisedChange
Daily Feed Cost₹'+cB.toFixed(1)+'₹'+cC.toFixed(1)+''+sg+'₹'+Math.abs(sD).toFixed(1)+'/day
Cost per Litre₹'+(cB/milk).toFixed(2)+'₹'+(cC/milk).toFixed(2)+''+sg+'₹'+Math.abs(cB/milk-cC/milk).toFixed(2)+'/L
Monthly Saving30 days'+sg+'₹'+Math.abs(sM).toFixed(0)+'
Annual Saving365 days'+sg+'₹'+Math.abs(sY).toFixed(0)+'
'; } let milkEconHtml=''; if(isLac&&milk>0&&baselineResult){ const cB=calcRationCost(baselineResult.schedule,baselineResult.asmm.dose_g); const dailyInc=milk*mp, pctB=dailyInc>0?cB/dailyInc*100:0; const col=pctB<80?'#166534':'#c0392b'; milkEconHtml='
' +'
Milk Economics
' +'' +'' +'' +'' +'
Milk income/day₹'+dailyInc.toFixed(1)+' (@₹'+mp.toFixed(2)+'/L × '+milk+'L)
Feed cost per litre₹'+(cB/milk).toFixed(2)+'
Feed cost % of income'+pctB.toFixed(1)+'% '+(pctB<80?'✓ Healthy':'⚠ Above 80%')+'
'; } const html='' +'Shunya FeedRight Diet Report' +'' +'
' +'' +'
Shunya Feed Right
'+esc(date)+'
' +'
' +'

Diet Advisory Report

' +'
ICAR 2013 · NDDB RBP Method
' +'
' +'
Animal:'+esc(aLabel)+'
' +'
Stage:'+esc(g.lifeStage)+'
' +'
Weight:'+bw+' kg
' +(isLac?'
Milk:'+milk+' L/day @ '+g.fat+'% fat
':'') +'
' +'
' +[{l:'DMI',v:g.dmiTarget.toFixed(2)+' kg'},{l:'TDN',v:(g.tdnTargetG/1000).toFixed(2)+' kg'},{l:'CP',v:g.cpTargetG.toFixed(0)+' g'},{l:'Ca',v:g.caTargetG.toFixed(1)+' g'},{l:'P',v:g.pTargetG.toFixed(1)+' g'}] .map(function(k){return'
'+k.l+'
'+k.v+'
';}).join('') +'
' +(baselineResult?'
Baseline Recommended Diet
'+rationRows(baselineResult)+'
FeedWet kgDM kgTDN gCP gCost
':'') +milkEconHtml +(customResult?'
Customised Diet
'+rationRows(customResult)+'
FeedWet kgDM kgTDN gCP gCost
':'') +savingsHtml +'
Generated by Shunya Feed Right BETA · ICAR 2013 / NDDB RBP · Advisory use only. Consult a veterinarian before changing your animal\'s diet.
' +''; // Open in new tab — user prints to PDF / shares from there const win=window.open('','_blank'); if(!win){showToast('Please allow popups for this site, then try again.');return;} win.document.write(html); win.document.close(); // On mobile, trigger print immediately so the share/save dialog appears win.onload=function(){ setTimeout(function(){win.print();},300); }; // If onload already fired (cached) if(win.document.readyState==='complete'){ setTimeout(function(){win.print();},400); } } function showToast(msg){ let t=document.getElementById('sfrToast'); if(!t){t=document.createElement('div');t.id='sfrToast';t.style.cssText='position:fixed;bottom:24px;left:50%;transform:translateX(-50%);background:#1a3a2a;color:#fff;padding:12px 20px;border-radius:8px;font-size:13px;z-index:99999;max-width:90vw;text-align:center;box-shadow:0 4px 16px rgba(0,0,0,0.25);transition:opacity 0.3s';document.body.appendChild(t);} t.textContent=msg;t.style.opacity='1';clearTimeout(t._t);t._t=setTimeout(()=>{t.style.opacity='0';},4000); } // Init onLifeStageChange(); FEEDS.forEach(f=>{baselineChecked[f.key]=true;}); buildSources(); // ── Static element event bindings (replaces all removed inline handlers) ── // Header: Sources button document.getElementById('sourcesModal') && void 0; // modal already exists document.querySelector('.sources-btn') && document.querySelector('.sources-btn').addEventListener('click', function(){ openModal('sourcesModal'); }); // Modal close buttons document.querySelector('#sourcesModal .modal-close') && document.querySelector('#sourcesModal .modal-close').addEventListener('click', function(){ closeModal('sourcesModal'); }); document.querySelector('#infoModal .modal-close') && document.querySelector('#infoModal .modal-close').addEventListener('click', function(){ closeModal('infoModal'); }); // Feed info image expand var infoImgPh = document.getElementById('infoImgPlaceholder'); if(infoImgPh) infoImgPh.addEventListener('click', function(){ expandFeedImage(this); }); // About accordion toggle (function(){ var aboutBtns = document.querySelectorAll('.container > div:first-child button'); aboutBtns.forEach(function(btn){ btn.addEventListener('click', function(){ var panel = this.nextElementSibling; panel.style.display = panel.style.display === 'none' ? 'block' : 'none'; var chev = this.querySelector('.chev'); if(chev) chev.textContent = chev.textContent === '▾' ? '▸' : '▾'; }); }); })(); // Step section header toggles ['step1Section','step2Section','step22Section','step3Section','step4Section','step5Section','econBaselineSection','econCustomSection'].forEach(function(id){ var section = document.getElementById(id); if(!section) return; var header = section.querySelector('.step-header'); if(header) header.addEventListener('click', function(){ toggleStep(id); }); }); // Animal type change var animalTypeEl = document.getElementById('animalType'); if(animalTypeEl) animalTypeEl.addEventListener('change', function(){ var fi = document.getElementById('fatPct'); if(!fi) return; if(this.value==='cow_indigenous') fi.value=4.5; else if(this.value==='cow_crossbred') fi.value=3.5; updateBreedNote(); }); // Life stage change var lifeStageEl = document.getElementById('lifeStage'); if(lifeStageEl) lifeStageEl.addEventListener('change', onLifeStageChange); // Step 1: Confirm Animal Details var confirmBtn = document.querySelector('#step1Section .btn-primary'); if(confirmBtn) confirmBtn.addEventListener('click', confirmAnimalDetails); // Step 2: Confirm Targets -> Select Feeds var step2Btn = document.querySelector('#step2Section .btn-primary'); if(step2Btn) step2Btn.addEventListener('click', openStep22); // Step 2.1: Calculate Baseline var baselineBtn = document.querySelector('#step22Section .btn-primary'); if(baselineBtn) baselineBtn.addEventListener('click', calculateBaseline); // Step 3: Customise + Check Economics (baseline) var customiseBtn = document.querySelector('#step3Section .btn-secondary'); if(customiseBtn) customiseBtn.addEventListener('click', openCustomize); var econBaseBtn = document.querySelector('#step3Section .btn-outline-gray'); if(econBaseBtn) econBaseBtn.addEventListener('click', function(){ openEconomics('baseline'); }); // Step 4: Redesign Diet var redesignBtn = document.querySelector('#step4Section .btn-purple'); if(redesignBtn) redesignBtn.addEventListener('click', recalculateCustom); // Step 5: Check Economics (custom) var econCustomBtn = document.querySelector('#step5Section .btn-purple'); if(econCustomBtn) econCustomBtn.addEventListener('click', function(){ openEconomics('custom'); }); // Export var exportBtnEl = document.getElementById('exportBtn'); if(exportBtnEl) exportBtnEl.addEventListener('click', exportPDF); // Modal overlay click-to-close document.querySelectorAll('.modal-overlay').forEach(function(el){ el.addEventListener('click', function(e){ if(e.target===this) closeModal(this.id); }); });