Legal Billing Rate Calculator

No professionals added yet.

`; container.innerHTML = html; return; } html += `
`; professionals.forEach(p => { html += ``; }); html += `
Name Salary Billable Hours Profit Margin Actions
${p.name} $${p.salary.toLocaleString()} ${p.hours.toLocaleString()} ${p.margin}%
`; container.innerHTML = html; } function handleAddUpdateProfessional() { const id = parseInt(profIdInput.value); const professional = { name: profNameInput.value.trim(), salary: parseFloat(profSalaryInput.value) || 0, hours: parseInt(profHoursInput.value) || 0, margin: parseFloat(profMarginInput.value) || 0 }; if (!professional.name || professional.salary <= 0 || professional.hours <= 0 || professional.margin < 0) { alert("Please fill in all fields with valid data."); return; } if (id) { // Update const index = professionals.findIndex(p => p.id === id); if (index !== -1) { professionals[index] = { ...professionals[index], ...professional }; } } else { // Add professional.id = professionals.length > 0 ? Math.max(...professionals.map(p => p.id)) + 1 : 1; professionals.push(professional); } resetForm(); renderProfessionalsTable(); calculateAllRates(); } function editProfessional(id) { const professional = professionals.find(p => p.id === id); if (!professional) return; profIdInput.value = professional.id; profNameInput.value = professional.name; profSalaryInput.value = professional.salary; profHoursInput.value = professional.hours; profMarginInput.value = professional.margin; document.getElementById('professional-form-title').textContent = 'Edit Professional'; addUpdateBtn.textContent = 'Update Professional'; cancelEditBtn.classList.remove('hidden'); } function deleteProfessional(id) { professionals = professionals.filter(p => p.id !== id); renderProfessionalsTable(); calculateAllRates(); } function cancelEdit() { resetForm(); } function resetForm() { profIdInput.value = ''; profNameInput.value = ''; profSalaryInput.value = ''; profHoursInput.value = ''; profMarginInput.value = ''; document.getElementById('professional-form-title').textContent = 'Add a Billable Professional'; addUpdateBtn.textContent = 'Add Professional'; cancelEditBtn.classList.add('hidden'); } // --- CALCULATION LOGIC --- function calculateAllRates() { const overheadPerProfessional = professionals.length > 0 ? firmData.overhead / professionals.length : 0; professionals.forEach(p => { const totalCost = p.salary + overheadPerProfessional; const costRate = p.hours > 0 ? totalCost / p.hours : 0; const billingRate = (p.margin >= 100 || p.margin < 0) ? costRate : costRate / (1 - p.margin / 100); p.costRate = costRate; p.billingRate = billingRate; p.overheadShare = overheadPerProfessional; }); updateDashboard(); } // --- UI & DASHBOARD UPDATE LOGIC --- function updateUI() { tabButtons.forEach((btn, index) => btn.classList.toggle('active', index === currentTabIndex)); tabPanels.forEach(panel => panel.classList.add('hidden')); document.getElementById(`tab-panel-${tabs[currentTabIndex]}`).classList.remove('hidden'); prevBtn.disabled = currentTabIndex === 0; prevBtn.classList.toggle('opacity-50', prevBtn.disabled); nextBtn.disabled = currentTabIndex === tabs.length - 1; nextBtn.classList.toggle('opacity-50', nextBtn.disabled); prevBtn.style.visibility = (currentTabIndex === 0) ? 'hidden' : 'visible'; tabIndicator.textContent = `Step ${currentTabIndex + 1} of ${tabs.length}`; } function updateDashboard() { const prompt = document.getElementById('dashboard-prompt'); const results = document.getElementById('dashboard-results'); if (professionals.length === 0) { prompt.classList.remove('hidden'); results.classList.add('hidden'); downloadPdfBtn.disabled = true; return; } prompt.classList.add('hidden'); results.classList.remove('hidden'); downloadPdfBtn.disabled = false; renderResultsTable(); updateCharts(); // Default breakdown chart to first professional if (professionals.length > 0) { updateBreakdownChart(professionals[0].id); } } function renderResultsTable() { const container = document.getElementById('results-table-container'); let html = `

Detailed Rate Breakdown

`; professionals.forEach(p => { const profitPerHour = p.billingRate - p.costRate; const annualProfit = profitPerHour * p.hours; html += ``; }); html += `
Name Cost Rate/hr Billing Rate/hr Profit/hr Annual Profit
${p.name} $${p.costRate.toFixed(2)} $${p.billingRate.toFixed(2)} $${profitPerHour.toFixed(2)} $${annualProfit.toLocaleString(undefined, {minimumFractionDigits: 2, maximumFractionDigits: 2})}
`; container.innerHTML = html; } // --- CHARTING --- function initializeCharts() { const ratesCtx = document.getElementById('rates-chart'); const breakdownCtx = document.getElementById('breakdown-chart'); if(!ratesCtx || !breakdownCtx) return; Chart.defaults.font.family = 'Inter'; ratesChart = new Chart(ratesCtx, { type: 'bar', data: { labels: [], datasets: [] }, options: { responsive: true, scales: { x: { grid: { display: false } }, y: { beginAtZero: true, ticks: { callback: (value) => '$' + value } } }, plugins: { legend: { position: 'top' } } } }); breakdownChart = new Chart(breakdownCtx, { type: 'doughnut', data: { labels: [], datasets: [] }, options: { responsive: true, plugins: { legend: { position: 'top' }, tooltip: { callbacks: { label: (context) => { let label = context.label || ''; if (label) label += ': '; if (context.parsed !== null) label += new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(context.parsed); return label; }}} } } }); } function updateCharts() { if (!ratesChart) return; ratesChart.data.labels = professionals.map(p => p.name); ratesChart.data.datasets = [ { label: 'Cost Rate ($/hr)', data: professionals.map(p => p.costRate.toFixed(2)), backgroundColor: '#fbbf24', // amber-400 borderRadius: 4, }, { label: 'Billing Rate ($/hr)', data: professionals.map(p => p.billingRate.toFixed(2)), backgroundColor: '#10b981', // emerald-500 borderRadius: 4, } ]; ratesChart.update(); } function updateBreakdownChart(id) { if (!breakdownChart) return; const p = professionals.find(prof => prof.id === id); if (!p) return; const salaryPerHour = p.hours > 0 ? p.salary / p.hours : 0; const overheadPerHour = p.hours > 0 ? p.overheadShare / p.hours : 0; const profitPerHour = p.billingRate - p.costRate; breakdownChart.data.labels = ['Salary Cost', 'Overhead Cost', 'Profit']; breakdownChart.data.datasets = [{ label: 'Rate Breakdown', data: [salaryPerHour, overheadPerHour, profitPerHour], backgroundColor: ['#6366f1', '#a5b4fc', '#10b981'], // indigo, light-indigo, emerald hoverOffset: 4 }]; breakdownChart.options.plugins.title = { display: true, text: `Rate Breakdown for ${p.name}` }; breakdownChart.update(); } // --- PDF GENERATION --- async function generatePdf() { const { jsPDF } = window.jspdf; const pdfContent = document.getElementById('pdf-content'); if (!pdfContent) return; try { const canvas = await html2canvas(pdfContent, { scale: 2, useCORS: true }); const imgData = canvas.toDataURL('image/png'); const pdf = new jsPDF({ orientation: 'landscape', unit: 'pt', format: 'a4' }); const pdfWidth = pdf.internal.pageSize.getWidth(); const pdfHeight = pdf.internal.pageSize.getHeight(); const imgProps = pdf.getImageProperties(imgData); const imgWidth = pdfWidth - 40; const imgHeight = (imgProps.height * imgWidth) / imgProps.width; let heightLeft = imgHeight; let position = 20; pdf.addImage(imgData, 'PNG', 20, position, imgWidth, imgHeight); heightLeft -= (pdfHeight - 40); while (heightLeft > 0) { position = heightLeft - imgHeight + 20; pdf.addPage(); pdf.addImage(imgData, 'PNG', 20, position, imgWidth, imgHeight); heightLeft -= (pdfHeight - 40); } pdf.save('Legal_Billing_Rate_Report.pdf'); } catch (error) { console.error('Error generating PDF:', error); alert('An error occurred while generating the PDF.'); } } // --- Expose functions to global scope for inline onclicks --- window.app = { editProfessional, deleteProfessional, updateBreakdownChart }; // --- START THE APP --- initializeTool(); });
Scroll to Top