Files
PayAnalyzerWebsite/global.js
T
2026-05-28 13:03:57 +00:00

212 lines
10 KiB
JavaScript

function calcBracketTax(taxable, brackets) {
let tax = 0, remaining = taxable;
for (let b of brackets) {
if (remaining <= 0) break;
let inBracket = Math.min(remaining, Math.max(0, b.max - b.min));
tax += inBracket * b.r;
remaining -= inBracket;
}
return tax;
}
function calcStateTax(taxable, stateCode) {
const s = STATE_DATA[stateCode] || {t:'none'};
if (s.t === 'none') return 0;
if (s.t === 'flat') return Math.max(0, taxable * s.v);
if (s.t === 'brackets') return calcBracketTax(taxable, s.v);
return 0;
}
function formatMoney(n) {
//return '$' + Math.round(n).toLocaleString('en-US');
return '$' + n.toFixed(2).toLocaleString('en-US');
}
let currentData = {};
function renderResults(d) {
const panel = document.getElementById('rightPanel');
panel.innerHTML = `
<div class="card fade-in">
<h2><span class="icon">💵</span> Paycheck Breakdown</h2>
<div class="results-grid">
<div class="result-box primary">
<div class="label">Net Take-Home</div>
<div class="value positive">${formatMoney(d.net)}</div>
<div class="sub">/year | ${formatMoney(d.netPer)} /${d.freq === 52 ? 'week' : d.freq === 26 ? '2 weeks' : d.freq === 24 ? '2 weeks' : d.freq === 12 ? 'month' : 'year'}</div>
</div>
<div class="result-box">
<div class="label">Federal Tax</div>
<div class="value negative">${formatMoney(d.fed)}</div>
<div class="sub effective-rate">${(d.fed/d.gross*100||0).toFixed(1)}% effective rate</div>
</div>
<div class="result-box">
<div class="label">FICA (SS + Medicare)</div>
<div class="value negative">${formatMoney(d.fica)}</div>
<div class="sub">SS: ${formatMoney(d.ss)} | Medicare: ${formatMoney(d.med + d.addMed)}</div>
</div>
<div class="result-box">
<div class="label">State Tax</div>
<div class="value negative">${formatMoney(d.stTax)}</div>
<div class="sub effective-rate">${(d.stTax/d.gross*100||0).toFixed(1)}% effective rate</div>
</div>
<div class="result-box">
<div class="label">City Tax</div>
<div class="value negative">${formatMoney(d.ctTax)}</div>
<div class="sub effective-rate">${(d.ctTax/d.gross*100||0).toFixed(1)}% effective rate</div>
</div>
<div class="result-box">
<div class="label">Retirement (401k)</div>
<div class="value accent">${formatMoney(d.retirement)}</div>
<div class="sub">${(d.retPct*100).toFixed(1)}% of gross</div>
</div>
<div class="result-box">
<div class="label">Other Deductions</div>
<div class="value">${formatMoney(d.preTax + d.postTax + d.other)}</div>
<div class="sub">Pre/Post/Other combined</div>
</div>
</div>
</div>
<div class="card fade-in">
<h2><span class="icon">📊</span> Allocation Breakdown</h2>
<table class="breakdown-table">
<thead><tr><th>Category</th><th>Annual</th><th>Per Pay</th><th>% of Gross</th></tr></thead>
<tbody>
<tr><td>Gross Pay</td><td class="amount">${formatMoney(d.gross)}</td><td class="amount">${formatMoney(d.gross/d.freq)}</td><td class="amount">100%</td></tr>
<tr><td>Retirement (401k)</td><td class="amount negative">${formatMoney(d.retirement)}</td><td class="amount">${formatMoney(d.retirement/d.freq)}</td><td class="amount">${(d.retPct*100).toFixed(1)}%</td></tr>
<tr><td>Pre-Tax Deductions</td><td class="amount negative">${formatMoney(d.preTax)}</td><td class="amount">${formatMoney(d.preTax/d.freq)}</td><td class="amount">${(d.preTax/d.gross*100).toFixed(1)}%</td></tr>
<tr><td>Federal Tax</td><td class="amount negative">${formatMoney(d.fed)}</td><td class="amount">${formatMoney(d.fed/d.freq)}</td><td class="amount">${(d.fed/d.gross*100).toFixed(1)}%</td></tr>
<tr><td>FICA (SS + Med)</td><td class="amount negative">${formatMoney(d.fica)}</td><td class="amount">${formatMoney(d.fica/d.freq)}</td><td class="amount">${(d.fica/d.gross*100).toFixed(1)}%</td></tr>
<tr><td>State Tax</td><td class="amount negative">${formatMoney(d.stTax)}</td><td class="amount">${formatMoney(d.stTax/d.freq)}</td><td class="amount">${(d.stTax/d.gross*100).toFixed(1)}%</td></tr>
<tr><td>City Tax</td><td class="amount negative">${formatMoney(d.ctTax)}</td><td class="amount">${formatMoney(d.ctTax/d.freq)}</td><td class="amount">${(d.ctTax/d.gross*100).toFixed(1)}%</td></tr>
<tr><td>Post-Tax & Other</td><td class="amount negative">${formatMoney(d.postTax + d.other)}</td><td class="amount">${formatMoney((d.postTax + d.other)/d.freq)}</td><td class="amount">${((d.postTax + d.other)/d.gross*100).toFixed(1)}%</td></tr>
<tr class="total-row"><td><strong>Net Take-Home</strong></td><td class="amount positive"><strong>${formatMoney(d.net)}</strong></td><td class="amount"><strong>${formatMoney(d.netPer)}</strong></td><td class="amount"><strong>${(d.net/d.gross*100).toFixed(1)}%</strong></td></tr>
</tbody>
</table>
</div>
<div class="card fade-in analysis-section">
<h3><span class="icon">🔄</span> Retirement Contribution Analysis</h3>
<div class="slider-container">
<label>401(k) Contribution <span id="sliderVal">${(d.retPct*100).toFixed(1)}%</span></label>
<input type="range" id="retSlider" min="0" max="25" step="0.5" value="${(d.retPct*100)}" oninput="updateAnalysis()">
</div>
<div class="comparison-table">
<table>
<thead><tr><th>Contribution %</th><th>Annual 401k</th><th>Net Pay / Period</th><th>Effective Tax Rate</th><th>Annual Take-Home</th></tr></thead>
<tbody id="analysisBody"></tbody>
</table>
</div>
<div class="chart-container"><canvas id="analysisChart"></canvas></div>
<div class="savings-summary">
<div class="savings-box">
<div class="sb-label">Annual Tax Shield</div>
<div class="sb-value positive">-${formatMoney(d.fed * d.retPct)}</div>
</div>
<div class="savings-box">
<div class="sb-label">Annual 401k Savings</div>
<div class="sb-value accent">${formatMoney(d.retirement)}</div>
</div>
</div>
</div>
`;
updateAnalysis();
}
function updateAnalysis() {
if (!currentData.gross) return;
const d = currentData;
const currentPct = parseFloat(document.getElementById('retSlider').value);
document.getElementById('sliderVal').textContent = currentPct.toFixed(1) + '%';
const rows = [];
const step = 1;
for (let p = 0; p <= 25; p += step) {
const pct = p / 100;
const retAmt = d.gross * pct;
const stateCode = document.getElementById('state').value;
const fedtaxable = Math.max(0, d.gross - d.stdDed - retAmt - d.preTax);
const taxable = Math.max(0, d.gross - retAmt - d.preTax);
const fed = calcBracketTax(fedtaxable, FED_BRACKETS[d.status]);
const fica = d.fica; // SS/Medicare don't change with 401k
//const stTax = d.stTax;
const stTax = calcStateTax(taxable, stateCode);
const cityPct = (parseFloat(document.getElementById('cityTax').value) / 100) || 0;
const ctTax = Math.max(0,taxable * cityPct);
const net = d.gross - retAmt - d.preTax - fed - fica - stTax - ctTax - d.postTax - d.other;
const netPer = net / d.freq;
const effRate = d.gross > 0 ? ((fed + fica + stTax + ctTax) / d.gross) * 100 : 0;
rows.push({pct, retAmt, netPer, effRate, net});
}
const tbody = document.getElementById('analysisBody');
tbody.innerHTML = rows.map(r => `
<tr class="${Math.abs(r.pct - currentPct) < 0.01 ? 'highlight-row' : ''}">
<td>${(r.pct*100).toFixed(1)}%</td>
<td>${formatMoney(r.retAmt)}</td>
<td>${formatMoney(r.netPer)}</td>
<td>${r.effRate.toFixed(1)}%</td>
<td>${formatMoney(r.net)}</td>
</tr>
`).join('');
drawChart(rows, currentPct);
}
function drawChart(rows, currentPct) {
const canvas = document.getElementById('analysisChart');
const ctx = canvas.getContext('2d');
const dpr = window.devicePixelRatio || 1;
const rect = canvas.parentElement.getBoundingClientRect();
canvas.width = rect.width * dpr;
canvas.height = rect.height * dpr;
ctx.scale(dpr, dpr);
const w = rect.width, h = rect.height;
ctx.clearRect(0, 0, w, h);
const maxVal = Math.max(...rows.map(r => r.net));
const barWidth = Math.min((w - 40) / rows.length * 0.6, 40);
const gap = (w - 40) / rows.length * 0.4;
rows.forEach((r, i) => {
const barH = (r.net / maxVal) * (h - 50);
const x = 20 + i * (barWidth + gap);
const y = h - 30 - barH;
ctx.fillStyle = Math.abs(r.pct - currentPct) < 0.005 ? '#6c5ce7' : '#2d3148';
ctx.beginPath();
ctx.roundRect(x, y, barWidth, barH, 4);
ctx.fill();
ctx.fillStyle = '#8b8fa3';
ctx.font = '11px sans-serif';
ctx.textAlign = 'center';
ctx.fillText((r.pct*100).toFixed(0) + '%', x + barWidth/2, h - 10);
if (Math.abs(r.pct - currentPct) < 0.005) {
ctx.fillStyle = '#a29bfe';
ctx.font = 'bold 12px sans-serif';
ctx.fillText(formatMoney(r.net), x + barWidth/2, y - 8);
}
});
}
// Wire up events
document.getElementById('calcBtn').addEventListener('click', calculate);
document.getElementById('retPct').addEventListener('input', (e) => {
document.getElementById('retPctDisplay').textContent = e.target.value + '%';
});
document.getElementById('cityTax').addEventListener('input', (e) => {
document.getElementById('cityTaxDisplay').textContent = e.target.value + '%';
});
// Auto-calculate on load
window.addEventListener('DOMContentLoaded', () => {
// Attach listeners to all inputs for auto-update
['grossPay','payCheck','payFreq','filingStatus','state','preTaxPay','postTaxPay','otherPay'].forEach(id => {
document.getElementById(id).addEventListener('input', calculate);
document.getElementById(id).addEventListener('change', calculate);
});
calculate();
});