Microgreens Profitability Calculator

Model seed + variable costs, monthly overhead, Margin of Safety, and full profitability. Multi-crop supported.

Step 0 — Global settings

Step 5 — Results

Monthly revenue
$0.00
Monthly net profit
$0.00
Gross margin
0%
Margin of Safety
0%

Step 4 — Margin of Safety (stress test)

Per month
Variable costs$0.00
Gross profit$0.00
Fixed overhead$0.00
Net profit$0.00
Break-even revenue$0.00
Break-even trays / month0
Stressed net profit$0.00
Stressed Margin of Safety0%
MOS = (Actual Sales − Break-even Sales) ÷ Actual Sales. A higher MOS means you can survive slower months more easily.
`; el.innerHTML = baseHTML; const rowsWrap = document.getElementById("mgpc-rows"); const currencySel = document.getElementById("mgpc-currency"); const fixedEl = document.getElementById("mgpc-fixed"); const autoScrollEl = document.getElementById("mgpc-autoscroll"); const dropEl = document.getElementById("mgpc-drop"); const costUpEl = document.getElementById("mgpc-costup"); const errEl = document.getElementById("mgpc-err"); let state = { currency: "USD", fixed: 350, rows: [ // prefill one row using Broccoli preset presetRow("Broccoli") ] }; function presetRow(name){ const p = VARIETIES.find(v=>v.name===name) || VARIETIES[0]; return { variety:p.name, tpw:10, days:p.days, seed_g:p.seed_g, seed_perkg:30, yield_oz:p.yield_oz, price_oz:p.price_per_oz, sellthrough:95, pack_tray:0.75, medium_tray:1.25, consum_tray:0.20, util_tray:0.30, labor_min:8, labor_hr:15 }; } function renderRows(){ rowsWrap.innerHTML = state.rows.map((_,i)=>rowTemplate(i)).join(""); // hydrate inputs state.rows.forEach((r,i)=>{ const card = rowsWrap.querySelector(`[data-row="${i}"]`); card.querySelector(`[data-k="variety"]`).value = r.variety; Object.keys(r).forEach(k=>{ const inp = card.querySelector(`[data-k="${k}"]`); if(inp && inp.tagName==="INPUT") inp.value = r[k]; }); // variety change -> apply preset (seed_g, days, yield, price) card.querySelector(`[data-k="variety"]`).addEventListener("change", (e)=>{ const v = e.target.value; const p = VARIETIES.find(x=>x.name===v); if(p){ r.variety = v; r.seed_g = p.seed_g; r.days = p.days; r.yield_oz = p.yield_oz; r.price_oz = p.price_per_oz; // push to UI card.querySelector(`[data-k="seed_g"]`).value = r.seed_g; card.querySelector(`[data-k="days"]`).value = r.days; card.querySelector(`[data-k="yield_oz"]`).value = r.yield_oz; card.querySelector(`[data-k="price_oz"]`).value = r.price_oz; } computeAndRender(true); }); // generic input change card.querySelectorAll("input").forEach(inp=>{ inp.addEventListener("input", ()=>{ const k = inp.getAttribute("data-k"); r[k] = Number(inp.value); computeAndRender(true); }); }); // remove card.querySelector(`[data-remove="${i}"]`).addEventListener("click", ()=>{ state.rows.splice(i,1); if(state.rows.length===0) state.rows.push(presetRow("Broccoli")); renderRows(); computeAndRender(false); }); }); } function safeNum(x){ return isFinite(x) ? x : 0; } function computeRow(r){ const sym = currencySymbols[state.currency] || "$"; const tpw = safeNum(r.tpw); const traysMonth = tpw * weeksPerMonth; // Seed cost: grams * (per kg / 1000) const seedCost = safeNum(r.seed_g) * (safeNum(r.seed_perkg)/1000); // Labor cost: minutes * hourly / 60 const laborCost = (safeNum(r.labor_min)/60) * safeNum(r.labor_hr); const varCostTray = seedCost + safeNum(r.pack_tray) + safeNum(r.medium_tray) + safeNum(r.consum_tray) + safeNum(r.util_tray) + laborCost; const sellFactor = Math.max(0, Math.min(1, safeNum(r.sellthrough)/100)); const revenueTray = safeNum(r.yield_oz) * safeNum(r.price_oz) * sellFactor; const gpTray = revenueTray - varCostTray; return { traysMonth, seedCost, laborCost, varCostTray, revenueTray, gpTray, revenueMonth: revenueTray * traysMonth, varMonth: varCostTray * traysMonth, gpMonth: gpTray * traysMonth, sym }; } function computeTotals(){ const fixed = safeNum(state.fixed); const sym = currencySymbols[state.currency] || "$"; const rows = state.rows.map(computeRow); const revenue = rows.reduce((a,b)=>a+b.revenueMonth,0); const varCost = rows.reduce((a,b)=>a+b.varMonth,0); const gp = revenue - varCost; const net = gp - fixed; const gmRatio = revenue>0 ? (gp/revenue) : 0; // Break-even revenue (fixed / gross margin ratio) const breakEvenRevenue = (gmRatio>0) ? (fixed/gmRatio) : Infinity; // Break-even trays/month estimate (fixed / weighted avg gross profit per tray) const totalTraysMonth = rows.reduce((a,b)=>a+b.traysMonth,0); const gpPerTrayWeighted = totalTraysMonth>0 ? (gp/totalTraysMonth) : 0; const breakEvenTrays = (gpPerTrayWeighted>0) ? (fixed/gpPerTrayWeighted) : Infinity; // Margin of Safety const mos = (revenue>0 && isFinite(breakEvenRevenue)) ? ((revenue - breakEvenRevenue)/revenue) : 0; // Stress test const drop = Math.max(0, Math.min(1, safeNum(Number(dropEl.value))/100)); const costUp = Math.max(0, safeNum(Number(costUpEl.value))/100); const stressedRevenue = revenue * (1 - drop); const stressedVar = varCost * (1 + costUp) * (1 - drop); // assume volume drops w/ sales const stressedGp = stressedRevenue - stressedVar; const stressedNet = stressedGp - fixed; const stressedGm = stressedRevenue>0 ? (stressedGp/stressedRevenue) : 0; const stressedBER = (stressedGm>0) ? (fixed/stressedGm) : Infinity; const stressedMOS = (stressedRevenue>0 && isFinite(stressedBER)) ? ((stressedRevenue - stressedBER)/stressedRevenue) : 0; return { sym, fixed, rows, revenue, varCost, gp, net, gmRatio, breakEvenRevenue, breakEvenTrays, mos, stressedNet, stressedMOS }; } function setText(id, txt){ const e=document.getElementById(id); if(e) e.textContent=txt; } function computeAndRender(fromInput){ errEl.style.display="none"; const t = computeTotals(); const sym = t.sym; // KPIs setText("k-rev", fmtMoney(t.revenue, sym)); setText("k-net", fmtMoney(t.net, sym)); setText("k-gm", Math.round(t.gmRatio*100) + "%"); setText("k-mos", Math.round(t.mos*100) + "%"); // Breakdown setText("r-var", fmtMoney(t.varCost, sym)); setText("r-gp", fmtMoney(t.gp, sym)); setText("r-fixed", fmtMoney(t.fixed, sym)); setText("r-net", fmtMoney(t.net, sym)); setText("r-ber", isFinite(t.breakEvenRevenue) ? fmtMoney(t.breakEvenRevenue, sym) : "—"); setText("r-bet", isFinite(t.breakEvenTrays) ? fmtNum(t.breakEvenTrays,0) : "—"); setText("r-snet", fmtMoney(t.stressedNet, sym)); setText("r-smos", Math.round(t.stressedMOS*100) + "%"); // Optional autoscroll if(fromInput && autoScrollEl.checked){ document.getElementById("mgpc-results").scrollIntoView({behavior:"smooth", block:"start"}); } // Basic sanity warning if(t.revenue<=0){ errEl.textContent = "Revenue is $0. Increase trays/week, yield, price, or sell-through."; errEl.style.display="block"; } } // Events currencySel.addEventListener("change", ()=>{ state.currency = currencySel.value; computeAndRender(false); }); fixedEl.addEventListener("input", ()=>{ state.fixed = Number(fixedEl.value); computeAndRender(true); }); dropEl.addEventListener("input", ()=>computeAndRender(true)); costUpEl.addEventListener("input", ()=>computeAndRender(true)); document.getElementById("mgpc-add").addEventListener("click", ()=>{ state.rows.push(presetRow("Radish")); renderRows(); computeAndRender(false); }); document.getElementById("mgpc-reset").addEventListener("click", ()=>{ state.currency="USD"; state.fixed=350; state.rows=[presetRow("Broccoli")]; currencySel.value="USD"; fixedEl.value="350"; dropEl.value="15"; costUpEl.value="10"; autoScrollEl.checked=false; renderRows(); computeAndRender(false); }); document.getElementById("mgpc-copy").addEventListener("click", async ()=>{ const t = computeTotals(); const sym = t.sym; const text = `Microgreens Profit Summary Monthly revenue: ${fmtMoney(t.revenue, sym)} Monthly variable costs: ${fmtMoney(t.varCost, sym)} Monthly gross profit: ${fmtMoney(t.gp, sym)} Monthly fixed overhead: ${fmtMoney(t.fixed, sym)} Monthly net profit: ${fmtMoney(t.net, sym)} Gross margin: ${Math.round(t.gmRatio*100)}% Margin of Safety: ${Math.round(t.mos*100)}%`; try{ await navigator.clipboard.writeText(text); alert("Copied summary to clipboard."); }catch(e){ alert(text); } }); // init currencySel.value = state.currency; fixedEl.value = state.fixed; renderRows(); computeAndRender(false); })();