Bonus Calculator and Analysis :root { --primary: #25364E; --accent: #EAF3F2; --border: #C7D3DA; --error: #b91c1c; --success: #15803d; --text-main: #25364E; --text-muted: #506A8D; } * { box-sizing: border-box; } body { font-family: 'poppins', system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; margin: 0; padding: 1.5rem; background: #EEF2F4; color: var(--text-main); } .container { max-width: 1000px; margin: 0 auto; background: #ffffff; border-radius: 12px; padding: 2rem; box-shadow: 0 10px 25px rgba(15, 23, 42, 0.08); } h1 { margin-top: 0; font-size: 1.8rem; margin-bottom: 0.4rem; } h2 { margin-top: 2rem; font-size: 1.25rem; margin-bottom: 0.4rem; } p.description { margin-top: 0; color: var(--text-muted); font-size: 0.95rem; } fieldset { border: 1px solid var(--border); border-radius: 10px; padding: 1rem 1.25rem 1rem; margin: 1rem 0 1.5rem; background: var(--accent); } fieldset legend { font-weight: 600; padding: 0 0.5rem; } .mode-options { display: flex; flex-wrap: wrap; gap: 1.5rem; margin-top: 0.5rem; } .mode-options label { display: flex; align-items: center; gap: 0.35rem; font-weight: 500; cursor: pointer; } .grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 1rem 1.5rem; } .form-group { margin-bottom: 0.75rem; } .form-group label { display: block; font-size: 0.9rem; font-weight: 500; margin-bottom: 0.25rem; } .form-group small { display: block; font-size: 0.8rem; color: var(--text-muted); margin-top: 0.15rem; } input[type="text"], input[type="number"], input[type="email"] { width: 100%; padding: 0.45rem 0.55rem; border: 1px solid var(--border); border-radius: 6px; font-size: 0.9rem; } input[type="number"] { text-align: right; } .currency-input { text-align: right; } input[readonly] { background-color: #F6F9F9; } table { width: 100%; border-collapse: collapse; font-size: 0.9rem; margin-top: 0.5rem; } th, td { border: 1px solid var(--border); padding: 0.4rem 0.5rem; text-align: right; } th { background: #F6F9F9; font-weight: 600; } th:first-child, td:first-child { text-align: left; width: 30%; } .note { font-size: 0.85rem; color: var(--text-muted); margin-top: 0.25rem; } .inline { display: inline-flex; align-items: center; gap: 0.25rem; } .bonus-options { display: flex; flex-wrap: wrap; gap: 1rem; margin-top: 0.5rem; } .bonus-options label { display: flex; align-items: center; gap: 0.35rem; font-size: 0.9rem; cursor: pointer; } .badge { display: inline-block; padding: 0.15rem 0.45rem; border-radius: 999px; font-size: 0.8rem; background: #E3F1F0; color: #25364E; font-weight: 500; } .required-star { color: var(--primary); font-weight: 700; margin-left: 0.15rem; } .team-member-row { display: grid; grid-template-columns: 1.8fr 1fr 1.1fr; gap: 0.5rem; margin-bottom: 0.4rem; } .team-member-header { display: grid; grid-template-columns: 1.8fr 1fr 1.1fr; gap: 0.5rem; font-size: 0.8rem; font-weight: 600; color: var(--text-muted); margin-bottom: 0.25rem; } .btn { display: inline-flex; align
Bonus Calculator and Analysis :root { --primary: #25364E; --accent: #EAF3F2; --border: #C7D3DA; --error: #b91c1c; --success: #15803d; --text-main: #25364E; --text-muted: #506A8D; } * { box-sizing: border-box; } body { font-family: 'poppins', system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; margin: 0; padding: 1.5rem; background: #EEF2F4; color: var(--text-main); } .container { max-width: 1000px; margin: 0 auto; background: #ffffff; border-radius: 12px; padding: 2rem; box-shadow: 0 10px 25px rgba(15, 23, 42, 0.08); } h1 { margin-top: 0; font-size: 1.8rem; margin-bottom: 0.4rem; } h2 { margin-top: 2rem; font-size: 1.25rem; margin-bottom: 0.4rem; } p.description { margin-top: 0; color: var(--text-muted); font-size: 0.95rem; } fieldset { border: 1px solid var(--border); border-radius: 10px; padding: 1rem 1.25rem 1rem; margin: 1rem 0 1.5rem; background: var(--accent); } fieldset legend { font-weight: 600; padding: 0 0.5rem; } .mode-options { display: flex; flex-wrap: wrap; gap: 1.5rem; margin-top: 0.5rem; } .mode-options label { display: flex; align-items: center; gap: 0.35rem; font-weight: 500; cursor: pointer; } .grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 1rem 1.5rem; } .form-group { margin-bottom: 0.75rem; } .form-group label { display: block; font-size: 0.9rem; font-weight: 500; margin-bottom: 0.25rem; } .form-group small { display: block; font-size: 0.8rem; color: var(--text-muted); margin-top: 0.15rem; } input[type="text"], input[type="number"], input[type="email"] { width: 100%; padding: 0.45rem 0.55rem; border: 1px solid var(--border); border-radius: 6px; font-size: 0.9rem; } input[type="number"] { text-align: right; } .currency-input { text-align: right; } input[readonly] { background-color: #F6F9F9; } table { width: 100%; border-collapse: collapse; font-size: 0.9rem; margin-top: 0.5rem; } th, td { border: 1px solid var(--border); padding: 0.4rem 0.5rem; text-align: right; } th { background: #F6F9F9; font-weight: 600; } th:first-child, td:first-child { text-align: left; width: 30%; } .note { font-size: 0.85rem; color: var(--text-muted); margin-top: 0.25rem; } .inline { display: inline-flex; align-items: center; gap: 0.25rem; } .bonus-options { display: flex; flex-wrap: wrap; gap: 1rem; margin-top: 0.5rem; } .bonus-options label { display: flex; align-items: center; gap: 0.35rem; font-size: 0.9rem; cursor: pointer; } .badge { display: inline-block; padding: 0.15rem 0.45rem; border-radius: 999px; font-size: 0.8rem; background: #E3F1F0; color: #25364E; font-weight: 500; } .required-star { color: var(--primary); font-weight: 700; margin-left: 0.15rem; } .team-member-row { display: grid; grid-template-columns: 1.8fr 1fr 1.1fr; gap: 0.5rem; margin-bottom: 0.4rem; } .team-member-header { display: grid; grid-template-columns: 1.8fr 1fr 1.1fr; gap: 0.5rem; font-size: 0.8rem; font-weight: 600; color: var(--text-muted); margin-bottom: 0.25rem; } .btn { display: inline-flex; align-items: center; justify-content: center; gap: 0.35rem; padding: 0.5rem 1.2rem; border-radius: 999px; border: none; cursor: pointer; font-size: 0.95rem; font-weight: 600; } .btn-primary { background: var(--primary); color: #ffffff; } .btn-secondary { background: #D9E2E8; color: #111827; } .btn-primary:disabled { opacity: 0.5; cursor: not-allowed; } .mt-1 { margin-top: 0.25rem; } .mt-2 { margin-top: 0.5rem; } .mt-3 { margin-top: 0.75rem; } .mt-4 { margin-top: 1rem; } .flex-between { display: flex; justify-content: space-between; align-items: center; gap: 0.75rem; flex-wrap: wrap; } .summary { padding: 0.75rem 0.85rem; border-radius: 8px; background: #F6F9F9; border: 1px dashed var(--border); font-size: 0.9rem; margin-top: 0.5rem; } .summary strong { font-weight: 700; } .error { margin-top: 0.35rem; color: var(--error); font-size: 0.85rem; } .success { margin-top: 0.35rem; color: var(--success); font-size: 0.9rem; } .divider { border-top: 1px solid var(--border); margin: 1.5rem 0; } @media (max-width: 640px) { .team-member-row, .team-member-header { grid-template-columns: 1fr 1fr; } .team-member-row input.member-bonus, .team-member-header div:nth-child(3) { grid-column: 1 / -1; } }

Bonus Calculator & Analysis

Use this tool to design your bonus structure or calculate this month’s payout and communicate it to your team as an increased hourly rate.

1. What would you like to do?

Practice Information

Bonus Analysis: Practice name, doctor name, postal address, and email are required.
Returning User: Practice name and email are required; doctor name is optional.

We’ll email your bonus report to this address.

Collections History (Bonus Analysis)

Enter your last four full calendar years of collections plus your last 12 months. The calculator will show average monthly collections and growth year to year.

Period Total Collections (Annual) Average Monthly Collections Growth vs Prior Period
Year 1 (oldest) - -
Year 2 - -
Year 3 - -
Year 4 (most recent full year) - -
Last 12 months - -
Average annual growth (based on the periods entered): -

Growth Goals & Bonus Tiers

The average annual growth for dental practices (both rural and urban) is typically between 5–15%. There are usually some unique circumstances that elevate growth beyond 10% on average across the nation. This can vary by practice due to the demographics you serve, the insurances you accept, and the services you offer.

We’ll start you with a default annual growth goal of 5%, but you can adjust this to fit your practice. We also want growth tiers to be realistic so your team can hit them at least 9 out of 12 months.

%
This will be used as your Tier 1 growth target. Tier 2 will automatically be set to Tier 1 + 5%.
Tier 1 growth: -
Tier 2 growth: -
Target annual collections: -
Target monthly collections: -
Additional annual collections vs last 12 months: -
Target annual collections: -
Target monthly collections: -
Additional annual collections vs last 12 months: -

Choose Bonus Payout Level

For Bonus Analysis, choose how generous you’d like to be at each tier. This shows what the monthly bonus pool would be based on your growth goals and collections.

%
Used only if “Custom” is selected above.
Estimated bonus pool this month: $0.00

Team Hours & Increased Hourly Bonus

Enter your team hours so we can convert the total bonus into an “increased hourly rate” for every person.

This should be the sum of all individual hours below. We’ll make sure they match before you can submit. Current sum of individual team hours: 0.00
Once collections and payout are entered, the hourly bonus will appear here.
Team Member Name
Hours Worked
Bonus Amount
The email report will include your calculations and the team’s increased hourly bonus.
// === EmailJS setup === (function() { emailjs.init({ publicKey: "UxcLVX1A9ZlItg8om", // your EmailJS public key }); })(); const SERVICE_ID = "service_7d0m1rh"; // Outlook service ID const TEMPLATE_ID = "template_e5objp2"; // Template ID // Helpers function normalizeNumberString(value) { if (value === undefined || value === null) return ""; return value.toString().replace(/[^0-9.\-]/g, ""); } function getNumber(value) { const cleaned = normalizeNumberString(value); const num = parseFloat(cleaned); return isNaN(num) ? 0 : num; } function formatCurrency(amount) { if (!isFinite(amount)) return ""; return "$" + amount.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 }); } function formatPercent(value) { if (!isFinite(value)) return "-"; return value.toFixed(2) + "%"; } const analysisState = { yearValues: [0, 0, 0, 0, 0], // oldest -> last 12 months avgGrowth: 0, tier1Monthly: 0, tier2Monthly: 0, bonusPercent: 0 }; let currentBonusPool = 0; document.addEventListener("DOMContentLoaded", function () { // Year labels based on current year const now = new Date(); const currentYear = now.getFullYear(); document.getElementById("year4_label").textContent = (currentYear - 4).toString(); document.getElementById("year3_label").textContent = (currentYear - 3).toString(); document.getElementById("year2_label").textContent = (currentYear - 2).toString(); document.getElementById("year1_label").textContent = (currentYear - 1).toString(); document.getElementById("last12_label").textContent = "Last 12 months"; // Mode switching document.querySelectorAll('input[name="mode"]').forEach(function (radio) { radio.addEventListener("change", function () { setMode(this.value); }); }); setMode("analysis"); // default // Attach currency formatting to collection fields const currencyInputs = [ "collections_year4", "collections_year3", "collections_year2", "collections_year1", "collections_last12", "return_collections" ]; currencyInputs.forEach(attachCurrencyFormatter); // Analysis listeners ["collections_year4", "collections_year3", "collections_year2", "collections_year1", "collections_last12" ].forEach(function (id) { document.getElementById(id).addEventListener("input", updateAnalysis); }); document.getElementById("desiredGrowth").addEventListener("input", updateGrowthTargets); document.querySelectorAll('input[name="bonus_structure"]').forEach(function (radio) { radio.addEventListener("change", updateBonusPoolFromAnalysis); }); document.getElementById("customBonusPercent").addEventListener("input", updateBonusPoolFromAnalysis); // Returning user listeners (inside Choose Bonus section) document.getElementById("return_collections").addEventListener("input", updateReturningBonusPool); document.getElementById("return_custom_percent").addEventListener("input", updateReturningBonusPool); document.querySelectorAll('input[name="return_bonus_structure"]').forEach(function (radio) { radio.addEventListener("change", updateReturningBonusPool); }); // Team hours document.getElementById("totalTeamHours").addEventListener("input", function () { updateTeamBonuses(); }); // Create 6 initial team rows for (let i = 0; i < 6; i++) { addTeamMemberRow(); } document.getElementById("addMemberBtn").addEventListener("click", function () { addTeamMemberRow(true); }); // Default growth value (5%) and initial targets const desiredGrowthInput = document.getElementById("desiredGrowth"); if (!desiredGrowthInput.value) { desiredGrowthInput.value = "5"; } updateGrowthTargets(); // Form submit document.getElementById("bonusForm").addEventListener("submit", handleFormSubmit); }); // Currency formatting for inputs function attachCurrencyFormatter(id) { const input = document.getElementById(id); if (!input) return; input.addEventListener("focus", function () { input.value = normalizeNumberString(input.value); input.select(); }); input.addEventListener("blur", function () { const num = getNumber(input.value); if (num) { input.value = formatCurrency(num); } else { input.value = ""; } if (id.startsWith("collections_")) { updateAnalysis(); } if (id === "return_collections") { updateReturningBonusPool(); } }); } function setMode(mode) { const analysisBlock = document.getElementById("analysisBlock"); const analysisBonusControls = document.getElementById("analysisBonusControls"); const returningBonusControls = document.getElementById("returningBonusControls"); const doctorInput = document.getElementById("doctorName"); const doctorStar = document.getElementById("doctorRequiredStar"); const addressInput = document.getElementById("practiceAddress"); const analysisOnlyEls = document.querySelectorAll(".analysis-only"); if (mode === "analysis") { // Show analysis content analysisBlock.style.display = ""; analysisBonusControls.style.display = ""; returningBonusControls.style.display = "none"; // Doctor + address required doctorInput.required = true; doctorStar.style.display = "inline"; addressInput.required = true; analysisOnlyEls.forEach(function (el) { el.style.display = ""; }); // Recalculate analysis-based bonus pool if needed updateAnalysis(); updateBonusPoolFromAnalysis(); } else { // Hide analysis content, show only Choose Bonus (returning) + team section analysisBlock.style.display = "none"; analysisBonusControls.style.display = "none"; returningBonusControls.style.display = ""; // Doctor optional, address hidden & not required doctorInput.required = false; doctorStar.style.display = "none"; addressInput.required = false; analysisOnlyEls.forEach(function (el) { el.style.display = "none"; }); // Recalculate returning-based bonus pool updateReturningBonusPool(); } } // === Analysis: collections and growth === function updateAnalysis() { const ids = [ "collections_year4", "collections_year3", "collections_year2", "collections_year1", "collections_last12" ]; const avgIds = [ "avg_year4", "avg_year3", "avg_year2", "avg_year1", "avg_last12" ]; const growthIds = [ "growth_year4", "growth_year3", "growth_year2", "growth_year1", "growth_last12" ]; const values = ids.map(function (id) { return getNumber(document.getElementById(id).value); }); analysisState.yearValues = values; const yoyRates = []; for (let i = 0; i < values.length; i++) { const annual = values[i]; const avgMonthly = annual / 12; document.getElementById(avgIds[i]).textContent = annual > 0 ? formatCurrency(avgMonthly) : "-"; if (i === 0 || annual <= 0 || values[i - 1] <= 0) { document.getElementById(growthIds[i]).textContent = "-"; } else { const growth = (annual / values[i - 1] - 1) * 100; yoyRates.push(growth); document.getElementById(growthIds[i]).textContent = formatPercent(growth); } } if (yoyRates.length) { const avgGrowth = yoyRates.reduce(function (sum, g) { return sum + g; }, 0) / yoyRates.length; analysisState.avgGrowth = avgGrowth; document.getElementById("avg_growth").textContent = formatPercent(avgGrowth); } else { analysisState.avgGrowth = 0; document.getElementById("avg_growth").textContent = "-"; } updateGrowthTargets(); } function updateGrowthTargets() { const baseAnnual = analysisState.yearValues[4] || 0; // last 12 months const goalGrowth = getNumber(document.getElementById("desiredGrowth").value || 5); const tier1Growth = goalGrowth; const tier2Growth = goalGrowth + 5; document.getElementById("tier1_growth_display").textContent = tier1Growth > 0 ? formatPercent(tier1Growth) : "-"; document.getElementById("tier2_growth_display").textContent = tier2Growth > 0 ? formatPercent(tier2Growth) : "-"; const tier1Annual = baseAnnual * (1 + tier1Growth / 100); const tier2Annual = baseAnnual * (1 + tier2Growth / 100); const tier1Monthly = tier1Annual / 12; const tier2Monthly = tier2Annual / 12; document.getElementById("tier1_annual_target").textContent = baseAnnual > 0 ? formatCurrency(tier1Annual) : "-"; document.getElementById("tier2_annual_target").textContent = baseAnnual > 0 ? formatCurrency(tier2Annual) : "-"; document.getElementById("tier1_monthly_target").textContent = baseAnnual > 0 ? formatCurrency(tier1Monthly) : "-"; document.getElementById("tier2_monthly_target").textContent = baseAnnual > 0 ? formatCurrency(tier2Monthly) : "-"; document.getElementById("tier1_incremental").textContent = baseAnnual > 0 ? formatCurrency(tier1Annual - baseAnnual) : "-"; document.getElementById("tier2_incremental").textContent = baseAnnual > 0 ? formatCurrency(tier2Annual - baseAnnual) : "-"; analysisState.tier1Monthly = isFinite(tier1Monthly) ? tier1Monthly : 0; analysisState.tier2Monthly = isFinite(tier2Monthly) ? tier2Monthly : 0; updateBonusPoolFromAnalysis(); } function updateBonusPoolFromAnalysis() { const selected = document.querySelector('input[name="bonus_structure"]:checked'); if (!selected) return; const modeRadio = document.querySelector('input[name="mode"]:checked'); if (!modeRadio || modeRadio.value !== "analysis") { // If we're not in analysis mode, don't override returning calculations return; } const mode = selected.value; const customPctInput = document.getElementById("customBonusPercent"); const baseTier1Monthly = analysisState.tier1Monthly || 0; const baseTier2Monthly = analysisState.tier2Monthly || 0; let bonusPool = 0; let bonusPercent = 0; if (mode === "tier1") { bonusPercent = 1; bonusPool = baseTier1Monthly * (bonusPercent / 100); } else if (mode === "tier2") { bonusPercent = 1.5; bonusPool = baseTier2Monthly * (bonusPercent / 100); } else if (mode === "custom") { const customPct = getNumber(customPctInput.value); bonusPercent = customPct; const baseMonthly = baseTier1Monthly || (analysisState.yearValues[4] || 0) / 12; bonusPool = baseMonthly * (bonusPercent / 100); } analysisState.bonusPercent = bonusPercent; setCurrentBonusPool(bonusPool); document.getElementById("analysis_bonus_pool_display").textContent = bonusPool > 0 ? formatCurrency(bonusPool) : "$0.00"; } // === Returning user bonus pool (inside Choose Bonus Payout) === function updateReturningBonusPool() { const modeRadio = document.querySelector('input[name="mode"]:checked'); if (!modeRadio || modeRadio.value !== "returning") { return; } const monthlyCollections = getNumber( document.getElementById("return_collections").value ); const selected = document.querySelector('input[name="return_bonus_structure"]:checked'); let payoutPercent = 0; if (selected) { if (selected.value === "custom") { payoutPercent = getNumber( document.getElementById("return_custom_percent").value ); } else { payoutPercent = parseFloat(selected.value) || 0; } } const pool = monthlyCollections * (payoutPercent / 100); document.getElementById("return_bonus_pool_display").textContent = pool > 0 ? formatCurrency(pool) : "$0.00"; setCurrentBonusPool(pool); } // Shared pool + team hourly function setCurrentBonusPool(pool) { currentBonusPool = isFinite(pool) && pool > 0 ? pool : 0; updateTeamBonuses(); } function addTeamMemberRow(scrollIntoView) { const container = document.getElementById("teamMembersContainer"); const index = container.children.length + 1; const row = document.createElement("div"); row.className = "team-member-row"; row.innerHTML = ` `; container.appendChild(row); const hoursInput = row.querySelector(".member-hours"); hoursInput.addEventListener("input", updateTeamBonuses); if (scrollIntoView) { row.scrollIntoView({ behavior: "smooth", block: "center" }); } } function updateTeamBonuses() { const container = document.getElementById("teamMembersContainer"); const hourInputs = container.querySelectorAll(".member-hours"); const bonusOutputs = container.querySelectorAll(".member-bonus"); const summaryEl = document.getElementById("hourlyIncreaseDisplay"); const calculatedHoursEl = document.getElementById("calculatedTeamHours"); let totalMemberHours = 0; hourInputs.forEach(function (input) { const h = getNumber(input.value); if (h > 0) totalMemberHours += h; }); // Live display of sum of individual hours if (calculatedHoursEl) { calculatedHoursEl.textContent = "Current sum of individual team hours: " + totalMemberHours.toFixed(2); } if (currentBonusPool > 0 && totalMemberHours > 0) { const hourlyIncrease = currentBonusPool / totalMemberHours; summaryEl.textContent = "Your team earned another " + formatCurrency(hourlyIncrease) + " per hour this month!"; summaryEl.dataset.hourlyIncrease = hourlyIncrease.toString(); hourInputs.forEach(function (input, idx) { const h = getNumber(input.value); const memberBonus = h * hourlyIncrease; bonusOutputs[idx].value = h > 0 ? formatCurrency(memberBonus) : ""; }); } else if (currentBonusPool > 0) { summaryEl.textContent = "Enter your team members and their hours to see the hourly bonus."; summaryEl.dataset.hourlyIncrease = ""; bonusOutputs.forEach(function (output) { output.value = ""; }); } else { summaryEl.textContent = "Once collections and payout are entered, the hourly bonus will appear here."; summaryEl.dataset.hourlyIncrease = ""; bonusOutputs.forEach(function (output) { output.value = ""; }); } validateHoursMatch(totalMemberHours); } function validateHoursMatch(totalMemberHoursParam) { const totalHoursEl = document.getElementById("totalTeamHours"); const errorEl = document.getElementById("hoursMismatchError"); const totalHoursVal = getNumber(totalHoursEl.value); let totalMemberHours = totalMemberHoursParam; if (typeof totalMemberHours !== "number") { totalMemberHours = 0; document .querySelectorAll("#teamMembersContainer .member-hours") .forEach(function (input) { totalMemberHours += getNumber(input.value); }); } if (totalHoursVal === 0 && totalMemberHours === 0) { errorEl.style.display = "none"; errorEl.textContent = ""; return false; } if (Math.abs(totalHoursVal - totalMemberHours) > 0.01) { errorEl.style.display = "block"; errorEl.textContent = "The total hours field must equal the sum of all team member hours. Please adjust before submitting."; return false; } else { errorEl.style.display = "none"; errorEl.textContent = ""; return true; } } // === Form submission & EmailJS send === function handleFormSubmit(event) { event.preventDefault(); const form = event.target; const formMessage = document.getElementById("formMessage"); formMessage.textContent = ""; formMessage.className = ""; if (!form.checkValidity()) { form.reportValidity(); return; } const hoursOk = validateHoursMatch(); if (!hoursOk) { formMessage.textContent = "Please fix the total hours so they match before requesting your email report."; formMessage.className = "error"; return; } if (currentBonusPool <= 0) { formMessage.textContent = "Please enter your collections and bonus % so we can calculate the bonus pool before submitting."; formMessage.className = "error"; return; } const mode = document.querySelector('input[name="mode"]:checked').value; const practiceName = document.getElementById("practiceName").value.trim(); const doctorName = document.getElementById("doctorName").value.trim(); const practiceAddress = document.getElementById("practiceAddress").value.trim(); const userEmail = document.getElementById("userEmail").value.trim(); const totalTeamHoursInput = getNumber( document.getElementById("totalTeamHours").value ); let totalMemberHours = 0; const teamMembers = []; document .querySelectorAll("#teamMembersContainer .team-member-row") .forEach(function (row) { const name = row.querySelector(".member-name").value.trim(); const hours = getNumber(row.querySelector(".member-hours").value); const bonusText = row.querySelector(".member-bonus").value; if (name || hours > 0) { teamMembers.push({ name: name, hours: hours, bonus_display: bonusText }); } totalMemberHours += hours; }); const hourlyIncreaseDisplay = document.getElementById("hourlyIncreaseDisplay"); const hourlyIncrease = getNumber(hourlyIncreaseDisplay.dataset.hourlyIncrease || "0"); // Build a nice HTML table for the email let teamMembersHtml = "

No team members were entered.

"; if (teamMembers.length > 0) { let rowsHtml = ""; teamMembers.forEach(function (member) { const safeName = member.name || "(no name entered)"; const hoursStr = member.hours.toFixed(2); const bonusStr = member.bonus_display || ""; rowsHtml += ` ${safeName} ${hoursStr} ${bonusStr} `; }); teamMembersHtml = ` <table cellpadding="6" cellspacing="0" border="1" style="border-collapse:collapse; font-size:14px; border-color:#d1d5db;"> Team member Hours worked Bonus amount ${rowsHtml} `; } // Base template params – included for BOTH modes so you always capture these const templateParams = { mode: mode, // "analysis" or "returning" practice_name: practiceName, doctor_name: doctorName, practice_address: practiceAddress, // blank/hidden in returning mode user_email: userEmail, reply_to: userEmail, total_team_hours_input: totalTeamHoursInput.toFixed(2), total_team_hours_sum: totalMemberHours.toFixed(2), bonus_pool: currentBonusPool.toFixed(2), hourly_increase: hourlyIncrease.toFixed(2), team_members_json: JSON.stringify(teamMembers), team_members_html: teamMembersHtml }; if (mode === "analysis") { const vals = analysisState.yearValues; templateParams.collections_year_oldest = vals[0]; templateParams.collections_year_2 = vals[1]; templateParams.collections_year_3 = vals[2]; templateParams.collections_year_4 = vals[3]; templateParams.collections_last_12 = vals[4]; templateParams.avg_growth_percent = analysisState.avgGrowth.toFixed(2); const desiredGrowth = getNumber( document.getElementById("desiredGrowth").value || 5 ); templateParams.desired_growth_percent = desiredGrowth.toFixed(2); templateParams.tier1_growth_percent = desiredGrowth.toFixed(2); templateParams.tier2_growth_percent = (desiredGrowth + 5).toFixed(2); const baseAnnual = vals[4] || 0; const tier1Annual = baseAnnual * (1 + desiredGrowth / 100); const tier2Annual = baseAnnual * (1 + (desiredGrowth + 5) / 100); templateParams.tier1_annual_target = tier1Annual.toFixed(2); templateParams.tier2_annual_target = tier2Annual.toFixed(2); templateParams.tier1_monthly_target = (tier1Annual / 12).toFixed(2); templateParams.tier2_monthly_target = (tier2Annual / 12).toFixed(2); templateParams.tier1_incremental = (tier1Annual - baseAnnual).toFixed(2); templateParams.tier2_incremental = (tier2Annual - baseAnnual).toFixed(2); templateParams.analysis_bonus_percent = analysisState.bonusPercent.toFixed(2); } else { const monthlyCollections = getNumber( document.getElementById("return_collections").value ); const selected = document.querySelector('input[name="return_bonus_structure"]:checked'); let payoutPercent = 0; if (selected) { if (selected.value === "custom") { payoutPercent = getNumber( document.getElementById("return_custom_percent").value ); } else { payoutPercent = parseFloat(selected.value) || 0; } } templateParams.return_monthly_collections = monthlyCollections.toFixed(2); templateParams.return_payout_percent = payoutPercent.toFixed(2); templateParams.return_bonus_pool = currentBonusPool.toFixed(2); } document.getElementById("submitBtn").disabled = true; formMessage.textContent = "Sending your bonus report..."; formMessage.className = "note"; emailjs.send(SERVICE_ID, TEMPLATE_ID, templateParams) .then(function () { formMessage.textContent = "Your bonus report has been emailed. Check your inbox shortly."; formMessage.className = "success"; }) .catch(function (error) { console.error("EmailJS error:", error); formMessage.textContent = "There was an error sending your report. Please check your entries and try again."; formMessage.className = "error"; }) .finally(function () { document.getElementById("submitBtn").disabled = false; }); }