Payroll Budget Planner

General Settings

Default Employer Contributions

No default contributions added yet.

Employee Payroll Details

No employees added yet.

Payroll Budget Summary

Complete data in previous tabs to see the summary.

No custom contributions added for this employee.

`; employeeDetailsContainer.appendChild(empDiv); const payTypeSelect = empDiv.querySelector('.pbp-emp-pay-type'); const salaryFields = empDiv.querySelector('.pbp-emp-salary-fields'); const hourlyFields = empDiv.querySelector('.pbp-emp-hourly-fields'); payTypeSelect.addEventListener('change', function() { salaryFields.style.display = this.value === 'Salary' ? 'block' : 'none'; hourlyFields.style.display = this.value === 'Hourly' ? 'block' : 'none'; }); payTypeSelect.dispatchEvent(new Event('change')); // Initialize const useCustomsCheckbox = empDiv.querySelector('.pbp-emp-use-defaults'); const customContainer = empDiv.querySelector(`#pbp-custom-contributions-container-${employeeIdCounter}`); const addCustomBtn = empDiv.querySelector('.pbp-add-custom-contribution'); useCustomsCheckbox.addEventListener('change', function() { const showCustom = !this.checked; customContainer.style.display = showCustom ? 'block' : 'none'; addCustomBtn.style.display = showCustom ? 'inline-block' : 'none'; }); empDiv.querySelector('.pbp-remove-employee').addEventListener('click', function() { this.closest('.pbp-employee-item').remove(); checkEmptyState(employeeDetailsContainer); }); addCustomBtn.addEventListener('click', function() { addCustomContributionToEmployee(this.closest('.pbp-employee-item').dataset.employeeId); }); updateAllCurrencyPrefixes(); checkEmptyState(employeeDetailsContainer); } function addCustomContributionToEmployee(empId, name = '', type = 'Percentage', value = '') { const container = document.getElementById(`pbp-custom-contributions-container-${empId}`); const customContributionId = Date.now(); // Unique enough for this purpose const itemDiv = document.createElement('div'); itemDiv.classList.add('pbp-repeatable-item', 'pbp-custom-contribution-item'); itemDiv.innerHTML = `
${type === 'Fixed' ? (currencySymbolInput.value || '$') : ''} ${type === 'Percentage' ? '%' : ''}
`; container.appendChild(itemDiv); itemDiv.querySelector('.pbp-remove-button').addEventListener('click', function() { this.parentElement.remove(); checkEmptyState(container); }); itemDiv.querySelector('.pbp-cc-type').addEventListener('change', function(e) { const valueInput = this.closest('.pbp-repeatable-item').querySelector('.pbp-cc-value'); const prefixSpan = this.closest('.pbp-repeatable-item').querySelector('.pbp-currency-prefix'); const suffixSpan = this.closest('.pbp-repeatable-item').querySelector('.pbp-input-group span:last-child'); if (e.target.value === 'Fixed') { prefixSpan.textContent = currencySymbolInput.value || '$'; suffixSpan.textContent = ''; valueInput.classList.add('pbp-currency-input'); } else { // Percentage prefixSpan.textContent = ''; suffixSpan.textContent = '%'; valueInput.classList.remove('pbp-currency-input'); } }); itemDiv.querySelector('.pbp-cc-type').dispatchEvent(new Event('change')); updateAllCurrencyPrefixes(); checkEmptyState(container); } // --- Calculation Logic --- const WEEKS_IN_YEAR = 52; const MONTHS_IN_YEAR = 12; function getNumEmployeePayPeriodsInBudgetPeriod(employeePayFreq, budgetPeriod) { let empPaysPerYear; switch (employeePayFreq) { case 'Weekly': empPaysPerYear = WEEKS_IN_YEAR; break; case 'Bi-Weekly': empPaysPerYear = WEEKS_IN_YEAR / 2; break; case 'Semi-Monthly': empPaysPerYear = MONTHS_IN_YEAR * 2; break; case 'Monthly': empPaysPerYear = MONTHS_IN_YEAR; break; default: empPaysPerYear = 0; } switch (budgetPeriod) { case 'Monthly': return empPaysPerYear / MONTHS_IN_YEAR; case 'Quarterly': return empPaysPerYear / 4; case 'Annually': return empPaysPerYear; default: return 0; } } function getGrossPayForBudgetPeriod(empData, budgetPeriod) { const numEmployees = empData.count; let singleEmployeeGross = 0; const weeksInBudgetPeriod = budgetPeriod === 'Monthly' ? WEEKS_IN_YEAR / MONTHS_IN_YEAR : budgetPeriod === 'Quarterly' ? WEEKS_IN_YEAR / 4 : WEEKS_IN_YEAR; if (empData.payType === 'Salary') { const annualSalary = empData.annualSalary || 0; singleEmployeeGross = budgetPeriod === 'Monthly' ? annualSalary / MONTHS_IN_YEAR : budgetPeriod === 'Quarterly' ? annualSalary / 4 : annualSalary; } else if (empData.payType === 'Hourly') { const hourlyRate = empData.hourlyRate || 0; const hoursWeek = empData.hoursWeek || 0; singleEmployeeGross = hourlyRate * hoursWeek * weeksInBudgetPeriod; } return singleEmployeeGross * numEmployees; } function collectInputData() { const data = { businessName: document.getElementById('pbp-business-name').value, currencySymbol: currencySymbolInput.value || '$', budgetPeriod: budgetPeriodSelect.value, defaultContributions: [], employees: [] }; document.querySelectorAll('.pbp-default-contribution-item').forEach(item => { data.defaultContributions.push({ name: item.querySelector('.pbp-dc-name').value.trim(), type: item.querySelector('.pbp-dc-type').value, value: parseFloat(item.querySelector('.pbp-dc-value').value) || 0 }); }); document.querySelectorAll('.pbp-employee-item').forEach(empItem => { const empId = empItem.dataset.employeeId; const employee = { id: empId, name: empItem.querySelector('.pbp-emp-name').value.trim(), count: parseInt(empItem.querySelector('.pbp-emp-count').value) || 1, payType: empItem.querySelector('.pbp-emp-pay-type').value, annualSalary: parseFloat(empItem.querySelector('.pbp-emp-annual-salary').value) || 0, hourlyRate: parseFloat(empItem.querySelector('.pbp-emp-hourly-rate').value) || 0, hoursWeek: parseFloat(empItem.querySelector('.pbp-emp-hours-week').value) || 0, payFrequency: empItem.querySelector('.pbp-emp-pay-freq').value, useDefaults: empItem.querySelector('.pbp-emp-use-defaults').checked, customContributions: [] }; if (!employee.useDefaults) { empItem.querySelectorAll(`#pbp-custom-contributions-container-${empId} .pbp-custom-contribution-item`).forEach(ccItem => { employee.customContributions.push({ name: ccItem.querySelector('.pbp-cc-name').value.trim(), type: ccItem.querySelector('.pbp-cc-type').value, value: parseFloat(ccItem.querySelector('.pbp-cc-value').value) || 0 }); }); } data.employees.push(employee); }); return data; } function generateSummary() { const inputData = collectInputData(); const summaryOutput = document.getElementById('pbp-summary-output'); let html = ''; const allContributionNames = new Set(); inputData.defaultContributions.forEach(dc => allContributionNames.add(dc.name)); inputData.employees.forEach(emp => { if (!emp.useDefaults) emp.customContributions.forEach(cc => allContributionNames.add(cc.name)); }); const contributionNamesArray = Array.from(allContributionNames); html += ``; contributionNamesArray.forEach(name => html += ``); html += ``; let overallTotalGrossPay = 0; let overallTotalContributions = 0; let overallGrandTotalPayroll = 0; const overallContributionTotals = {}; // For summary by contribution type contributionNamesArray.forEach(name => overallContributionTotals[name] = 0); inputData.employees.forEach(emp => { const grossPayForPeriod = getGrossPayForBudgetPeriod(emp, inputData.budgetPeriod); let employeeTotalContributions = 0; const contributionsForThisEmployee = {}; // Store calculated amounts for columns const activeContributions = emp.useDefaults ? inputData.defaultContributions : emp.customContributions; activeContributions.forEach(contrib => { if(!contrib.name) return; // Skip if contribution name is empty let contribAmount = 0; if (contrib.type === 'Percentage') { contribAmount = (grossPayForPeriod / emp.count) * (contrib.value / 100); // Per single employee in group } else { // Fixed Amount const numPaysInBudget = getNumEmployeePayPeriodsInBudgetPeriod(emp.payFrequency, inputData.budgetPeriod); contribAmount = contrib.value * numPaysInBudget; // Per single employee in group } contribAmount *= emp.count; // Total for the group contributionsForThisEmployee[contrib.name] = (contributionsForThisEmployee[contrib.name] || 0) + contribAmount; employeeTotalContributions += contribAmount; overallContributionTotals[contrib.name] = (overallContributionTotals[contrib.name] || 0) + contribAmount; }); const totalPayrollCostForEmployee = grossPayForPeriod + employeeTotalContributions; overallTotalGrossPay += grossPayForPeriod; overallTotalContributions += employeeTotalContributions; overallGrandTotalPayroll += totalPayrollCostForEmployee; html += ``; contributionNamesArray.forEach(name => { html += ``; }); html += ``; }); html += ``; contributionNamesArray.forEach(name => { html += ``; }); html += ` `; html += `
Employee/Group#Gross Pay (${inputData.budgetPeriod})${name}Total ContributionsTotal Payroll Cost
${emp.name || `Employee/Group ${emp.id}`} ${emp.count} ${inputData.currencySymbol}${grossPayForPeriod.toFixed(2)}${inputData.currencySymbol}${(contributionsForThisEmployee[name] || 0).toFixed(2)}${inputData.currencySymbol}${employeeTotalContributions.toFixed(2)} ${inputData.currencySymbol}${totalPayrollCostForEmployee.toFixed(2)}
Subtotals ${inputData.currencySymbol}${overallTotalGrossPay.toFixed(2)}${inputData.currencySymbol}${(overallContributionTotals[name] || 0).toFixed(2)}${inputData.currencySymbol}${overallTotalContributions.toFixed(2)} ${inputData.currencySymbol}${overallGrandTotalPayroll.toFixed(2)}
`; html += `

Overall Budget Summary (${inputData.budgetPeriod})

`; html += ``; contributionNamesArray.forEach(name => { if(overallContributionTotals[name] > 0) { // Only show if there's a value html += ``; } }); html += `
Total Gross Pay:${inputData.currencySymbol}${overallTotalGrossPay.toFixed(2)}
Total - ${name}:${inputData.currencySymbol}${(overallContributionTotals[name] || 0).toFixed(2)}
Total Employer Contributions:${inputData.currencySymbol}${overallTotalContributions.toFixed(2)}
GRAND TOTAL PAYROLL BUDGET:${inputData.currencySymbol}${overallGrandTotalPayroll.toFixed(2)}
`; summaryOutput.innerHTML = html; downloadPdfButton.disabled = inputData.employees.length === 0; } // PDF Download downloadPdfButton.addEventListener('click', function () { if (typeof jspdf === 'undefined' || typeof jspdf.jsPDF === 'undefined') { alert('Error: jsPDF library is not loaded.'); return; } const { jsPDF } = jspdf; const doc = new jsPDF('landscape'); // Landscape for wider tables const inputData = collectInputData(); const primaryColor = getComputedStyle(document.documentElement).getPropertyValue('--pbp-primary-color').trim(); const secondaryColor = getComputedStyle(document.documentElement).getPropertyValue('--pbp-secondary-color').trim(); const textColor = getComputedStyle(document.documentElement).getPropertyValue('--pbp-text-color').trim(); const buttonTextColor = getComputedStyle(document.documentElement).getPropertyValue('--pbp-button-text-color').trim(); doc.setFontSize(18); doc.setTextColor(primaryColor); doc.text('Payroll Budget Plan', doc.internal.pageSize.getWidth() / 2, 20, { align: 'center' }); doc.setFontSize(12); doc.setTextColor(textColor); doc.text(`Business Name: ${inputData.businessName || 'N/A'}`, 14, 30); doc.text(`Budget Period: ${inputData.budgetPeriod}`, 14, 37); doc.text(`Currency: ${inputData.currencySymbol}`, 14, 44); let lastY = 50; // Default Contributions Table (if any) if (inputData.defaultContributions.length > 0) { doc.setFontSize(14); doc.setTextColor(primaryColor); doc.text('Default Employer Contributions:', 14, lastY); lastY += 7; const dcHeaders = [['Contribution Name', 'Type', 'Value']]; const dcBody = inputData.defaultContributions.map(dc => [ dc.name, dc.type, dc.type === 'Percentage' ? `${dc.value.toFixed(2)}%` : `${inputData.currencySymbol}${dc.value.toFixed(2)}` ]); doc.autoTable({ startY: lastY, head: dcHeaders, body: dcBody, theme: 'grid', headStyles: { fillColor: secondaryColor, textColor: buttonTextColor } }); lastY = doc.lastAutoTable.finalY + 10; } doc.setFontSize(14); doc.setTextColor(primaryColor); doc.text('Payroll Details:', 14, lastY); lastY += 7; const contributionNamesArray = Array.from(new Set( inputData.defaultContributions.map(dc => dc.name) .concat(...inputData.employees.filter(e => !e.useDefaults).map(e => e.customContributions.map(cc => cc.name))) .filter(name => name) // Filter out empty names )); const headers = [['Employee/Group', '#', `Gross Pay (${inputData.budgetPeriod})`, ...contributionNamesArray, 'Total Contributions', 'Total Payroll Cost']]; const body = []; let overallTotalGrossPay = 0; let overallTotalContributions = 0; let overallGrandTotalPayroll = 0; const overallContributionTotals = {}; contributionNamesArray.forEach(name => overallContributionTotals[name] = 0); inputData.employees.forEach(emp => { const row = []; row.push(emp.name || `Employee/Group ${emp.id}`); row.push(emp.count); const grossPayForPeriod = getGrossPayForBudgetPeriod(emp, inputData.budgetPeriod); row.push(`${inputData.currencySymbol}${grossPayForPeriod.toFixed(2)}`); overallTotalGrossPay += grossPayForPeriod; let employeeTotalContributions = 0; const contributionsForThisEmployee = {}; const activeContributions = emp.useDefaults ? inputData.defaultContributions : emp.customContributions; activeContributions.forEach(contrib => { if(!contrib.name) return; let contribAmount = 0; if (contrib.type === 'Percentage') { contribAmount = (grossPayForPeriod / emp.count) * (contrib.value / 100); } else { const numPaysInBudget = getNumEmployeePayPeriodsInBudgetPeriod(emp.payFrequency, inputData.budgetPeriod); contribAmount = contrib.value * numPaysInBudget; } contribAmount *= emp.count; contributionsForThisEmployee[contrib.name] = (contributionsForThisEmployee[contrib.name] || 0) + contribAmount; employeeTotalContributions += contribAmount; overallContributionTotals[contrib.name] = (overallContributionTotals[contrib.name] || 0) + contribAmount; }); contributionNamesArray.forEach(name => { row.push(`${inputData.currencySymbol}${(contributionsForThisEmployee[name] || 0).toFixed(2)}`); }); row.push(`${inputData.currencySymbol}${employeeTotalContributions.toFixed(2)}`); overallTotalContributions += employeeTotalContributions; const totalPayrollCostForEmployee = grossPayForPeriod + employeeTotalContributions; row.push(`${inputData.currencySymbol}${totalPayrollCostForEmployee.toFixed(2)}`); overallGrandTotalPayroll += totalPayrollCostForEmployee; body.push(row); }); // Add Subtotal Row to PDF body const subtotalRow = ['Subtotals', inputData.employees.reduce((sum, e) => sum + e.count, 0) || '', `${inputData.currencySymbol}${overallTotalGrossPay.toFixed(2)}`]; contributionNamesArray.forEach(name => subtotalRow.push(`${inputData.currencySymbol}${(overallContributionTotals[name] || 0).toFixed(2)}`)); subtotalRow.push(`${inputData.currencySymbol}${overallTotalContributions.toFixed(2)}`); subtotalRow.push(`${inputData.currencySymbol}${overallGrandTotalPayroll.toFixed(2)}`); body.push(subtotalRow.map(val => ({ content: val, styles: { fontStyle: 'bold', fillColor: '#f0f0f0' } }))); doc.autoTable({ startY: lastY, head: headers, body: body, theme: 'grid', headStyles: { fillColor: secondaryColor, textColor: buttonTextColor, fontStyle: 'bold' }, columnStyles: { // Adjust column widths as needed 0: { cellWidth: 'auto' }, // Emp Name 1: { cellWidth: 15, halign: 'right' }, // # 2: { cellWidth: 30, halign: 'right' }, // Gross }, didParseCell: function (data) { // Right align numeric columns const numericHeaders = ['#', `Gross Pay (${inputData.budgetPeriod})`, ...contributionNamesArray, 'Total Contributions', 'Total Payroll Cost']; if (data.row.index > -1 && numericHeaders.includes(data.column.dataKey) ) { // data.row.index > -1 for body cells if (data.cell.raw && typeof data.cell.raw === 'string' && data.cell.raw.startsWith(inputData.currencySymbol)) { data.cell.styles.halign = 'right'; } else if (!isNaN(parseFloat(data.cell.raw))) { data.cell.styles.halign = 'right'; } } if (data.row.section === 'head' && typeof data.column.dataKey === 'number' && data.column.dataKey > 0) { // Align header text for numbers to right data.cell.styles.halign = 'right'; } } }); lastY = doc.lastAutoTable.finalY + 10; // Summary Section doc.setFontSize(14); doc.setTextColor(primaryColor); doc.text(`Overall Budget Summary (${inputData.budgetPeriod}):`, 14, lastY); lastY += 7; const summaryBody = [ [{ content: 'Total Gross Pay:', styles: { fontStyle: 'bold'} }, { content: `${inputData.currencySymbol}${overallTotalGrossPay.toFixed(2)}`, styles: { halign: 'right'} }], ]; contributionNamesArray.forEach(name => { if (overallContributionTotals[name] > 0) { summaryBody.push([ { content: `Total - ${name}:`, styles: { fontStyle: 'normal'} }, { content: `${inputData.currencySymbol}${(overallContributionTotals[name] || 0).toFixed(2)}`, styles: { halign: 'right'} } ]); } }); summaryBody.push( [{ content: 'Total Employer Contributions:', styles: { fontStyle: 'bold'} }, { content: `${inputData.currencySymbol}${overallTotalContributions.toFixed(2)}`, styles: { halign: 'right'} }], [{ content: 'GRAND TOTAL PAYROLL BUDGET:', styles: { fontStyle: 'bold', fillColor: primaryColor, textColor: buttonTextColor} }, { content: `${inputData.currencySymbol}${overallGrandTotalPayroll.toFixed(2)}`, styles: { fontStyle: 'bold', halign: 'right', fillColor: primaryColor, textColor: buttonTextColor} }] ); doc.autoTable({ startY: lastY, body: summaryBody, theme: 'plain', // No grid for summary, or 'grid' styles: { fontSize: 10 }, columnStyles: { 0: { cellWidth: 100 }, 1: { cellWidth: 50, halign: 'right'} }, }); doc.save(`Payroll_Budget_Plan_${inputData.businessName.replace(/\s+/g, '_') || 'Report'}.pdf`); }); checkEmptyState(defaultContributionsContainer); // Initial check for default contributions checkEmptyState(employeeDetailsContainer); // Initial check for employees showTab(0); // Initialize first tab updateAllCurrencyPrefixes(); // Set initial currency symbols });
Scroll to Top