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.
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
For Returning User Bonus Calculator, enter this month’s collections and choose
the percentage of collections you’re paying out as a bonus. The calculator will
automatically show the team bonus pool for this month.
Custom %:
If “Custom” is selected, we’ll use this percentage.
Bonus pool based on your entries:
$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 = "