1window.calculatorHandlers = window.calculatorHandlers || {}; 2window.calculatorOutputs = window.calculatorOutputs || {}; 3window.calculatorHandlers.mortgage_payoff = function ($form, $container, response = null) { 4 function show_error(text) { 5 let errorContainer = $container.find(".out-error-message"); 6 let resultsContainer = $container.find(".wwd-calculator-results"); 7 8 if (text === "") { 9 resultsContainer.show(); 10 errorContainer.hide(); 11 } else { 12 resultsContainer.hide(); 13 errorContainer.text(text).show(); 14 } 15 } 16 17 // Get input values 18 var loanType = $form.find('input[name="loan_type"]:checked').val(); 19 var currentMortgageBalance = parseFloat($form.find("#current_mortgage_balance").val().replace(/,/g, "")); 20 var yearsLeft = parseFloat($form.find("#years_left").val()); 21 var monthsLeft = parseFloat($form.find("#months_left").val()); 22 var loanTerm = loanType === "new" ? parseInt($form.find('input[name="loan_term"]:checked').val()) : yearsLeft + monthsLeft / 12; 23 var interestRate = parseFloat($form.find("#interest_rate").val().replace(/,/g, "")) / 100; 24 var extraPaymentAmount = parseFloat($form.find("#extra_payment_amount").val().replace(/,/g, "")) || 0; 25 var extraPaymentFrequency = $form.find('input[name="extra_payment_frequency"]:checked').val(); 26 27 // Perform calculations 28 var loanAmount = currentMortgageBalance; 29 30 switch (loanType) { 31 case "new": 32 var numberOfPayments = loanTerm * 12; 33 break; 34 case "existing": 35 var numberOfPayments = yearsLeft * 12 + monthsLeft; 36 break; 37 } 38 39 // Calculate monthly principal and interest 40 var monthlyInterestRate = interestRate / 12; 41 var monthlyPandI = (loanAmount * monthlyInterestRate * Math.pow(1 + monthlyInterestRate, numberOfPayments)) / (Math.pow(1 + monthlyInterestRate, numberOfPayments) - 1); 42 43 // Calculate amortization schedule 44 var amortizationSchedule = []; 45 var remainingBalance = loanAmount; 46 var remainingBalanceWithExtra = loanAmount; 47 var totalPrincipal = 0; 48 var totalInterest = 0; 49 var totalPrincipalWithExtra = 0; 50 var totalInterestWithExtra = 0; 51 var loanTermMonthsWithExtra = 0; 52 53 for (var month = 1; month <= numberOfPayments; month++) { 54 // Regular payment calculations 55 var monthlyInterest = remainingBalance * monthlyInterestRate; 56 var principalPayment = monthlyPandI - monthlyInterest; 57 remainingBalance = Math.max(0, remainingBalance - principalPayment); 58 totalPrincipal += principalPayment; 59 totalInterest += monthlyInterest; 60 61 // Extra payment calculations 62 var extraPaymentThisMonth = 0; 63 64 switch (extraPaymentFrequency) { 65 case "monthly": 66 extraPaymentThisMonth = extraPaymentAmount; 67 break; 68 case "quarterly": 69 if (month % 3 === 0) extraPaymentThisMonth = extraPaymentAmount; 70 break; 71 case "semiannually": 72 if (month % 6 === 0) extraPaymentThisMonth = extraPaymentAmount; 73 break; 74 case "semi-annually": 75 if (month % 6 === 0) extraPaymentThisMonth = extraPaymentAmount; 76 break; 77 case "annually": 78 if (month % 12 === 0) extraPaymentThisMonth = extraPaymentAmount; 79 break; 80 } 81 82 var monthlyInterestWithExtra = remainingBalanceWithExtra * monthlyInterestRate; 83 var principalPaymentWithExtra = Math.min(remainingBalanceWithExtra, monthlyPandI - monthlyInterestWithExtra + extraPaymentThisMonth); 84 remainingBalanceWithExtra = Math.max(0, remainingBalanceWithExtra - principalPaymentWithExtra); 85 totalPrincipalWithExtra += principalPaymentWithExtra; 86 totalInterestWithExtra += monthlyInterestWithExtra; 87 var paymentWithExtra = principalPaymentWithExtra + monthlyInterestWithExtra; 88 89 amortizationSchedule.push({ 90 month: month, 91 payment: monthlyPandI, 92 principalPayment: principalPayment, 93 interestPayment: monthlyInterest, 94 remainingBalance: remainingBalance, 95 totalPrincipalPaid: totalPrincipal, 96 totalInterestPaid: totalInterest, 97 extraPayment: extraPaymentThisMonth, 98 paymentWithExtra: paymentWithExtra, 99 principalPaymentWithExtra: principalPaymentWithExtra, 100 interestPaymentWithExtra: monthlyInterestWithExtra, 101 remainingBalanceWithExtra: remainingBalanceWithExtra, 102 totalPrincipalPaidWithExtra: totalPrincipalWithExtra, 103 totalInterestPaidWithExtra: totalInterestWithExtra, 104 totalSaved: totalInterest - totalInterestWithExtra, 105 }); 106 107 if (remainingBalanceWithExtra <= 0 && loanTermMonthsWithExtra === 0) { 108 loanTermMonthsWithExtra = month; 109 } 110 } 111 112 // Prepare result object 113 var result = { 114 loanType: loanType, 115 loanAmount: loanAmount, 116 interestRate: interestRate, 117 loanTermMonths: numberOfPayments, 118 loanTermMonthsWithExtra: loanTermMonthsWithExtra,
119 monthlyPandI: monthlyPandI, 120 totalPrincipal: totalPrincipal, 121 totalPrincipalWithExtra: totalPrincipalWithExtra, 122 totalInterest: totalInterest, 123 totalInterestWithExtra: totalInterestWithExtra, 124 amortizationSchedule: amortizationSchedule, 125 }; 126 127 // Update the UI with the results 128 updateUI(result, $container); 129 130 // Clear any previous errors 131 show_error(""); 132 133 function updateUI(result, $container) { 134 // Add to window.wwd_results 135 window.wwd_results = window.wwd_results || {}; 136 137 for (let key in result) { 138 if (!Array.isArray(result[key])) { 139 window.wwd_results[key] = result[key]; 140 } 141 } 142 // time saved text 143 var timeSaved = result.loanTermMonths - result.loanTermMonthsWithExtra; 144 var years = Math.floor(timeSaved / 12); 145 var months = timeSaved % 12; 146 var timeSavedText = ""; 147 var timeSavedFullText = ""; 148 if (years > 0) { 149 timeSavedText += years + " Year" + (years > 1 ? "s" : ""); 150 timeSavedFullText += years + " year" + (years > 1 ? "s" : ""); 151 } 152 if (months > 0) { 153 timeSavedText += (years > 0 ? " & " : "") + months + " Month" + (months > 1 ? "s" : ""); 154 timeSavedFullText += (years > 0 ? " and " : "") + months + " month" + (months > 1 ? "s" : ""); 155 } 156 timeSavedFullText += " earlier"; 157 158 var paidOffTimeframeText = ""; 159 payoffYears = Math.floor(result.loanTermMonthsWithExtra / 12); 160 payoffMonths = result.loanTermMonthsWithExtra % 12; 161 if (payoffYears > 0) { 162 paidOffTimeframeText += payoffYears + " year" + (payoffYears > 1 ? "s" : ""); 163 } 164 if (payoffMonths > 0) { 165 paidOffTimeframeText += (payoffYears > 0 ? " and " : "") + payoffMonths + " month" + (payoffMonths > 1 ? "s" : ""); 166 } 167 168 // Summary tab 169 $container.find("#out-estimated-term-savings, .out-time-saved").text(timeSavedText); 170 $container.find("#out-time-saved-text").text(timeSavedFullText); 171 172 $container.find(".out-interest-savings").text("$" + wwd_moneyFormat(result.totalInterest - result.totalInterestWithExtra, 0)); 173 $container.find(".out-extra-amount").text("$" + wwd_moneyFormat(extraPaymentAmount, 0)); 174 $container.find(".out-total-term-with-extra").text(paidOffTimeframeText); 175 176 updateInterestOverTimeChart(result, $container.find("#out-interest-over-time-chart")); 177 178 // Full Report tab 179 // Update Full Report tab elements 180 $container.find(".out-interest-savings").text("$" + wwd_moneyFormat(result.totalInterest - result.totalInterestWithExtra, 0)); 181 $container.find(".out-total-interest-original").text("$" + wwd_moneyFormat(result.totalInterest, 0)); 182 $container.find(".out-total-interest-with-extra").text("$" + wwd_moneyFormat(result.totalInterestWithExtra, 0)); 183 $container.find(".out-principal-interest-original").text("$" + wwd_moneyFormat(result.totalInterest + result.loanAmount, 0)); 184 $container.find(".out-principal-interest-with-extra").text("$" + wwd_moneyFormat(result.totalInterestWithExtra + result.loanAmount, 0)); 185 186 // Calculate and update payoff dates 187 const startDate = new Date(); 188 const originalPayoffDate = new Date(startDate.getFullYear(), startDate.getMonth() + result.loanTermMonths, 1); 189 const newPayoffDate = new Date(startDate.getFullYear(), startDate.getMonth() + result.loanTermMonthsWithExtra, 1); 190 191 $container.find(".out-payoff-date-original").text(originalPayoffDate.toLocaleString("default", { month: "long", year: "numeric" })); 192 $container.find(".out-payoff-date-with-extra").text(newPayoffDate.toLocaleString("default", { month: "long", year: "numeric" })); 193 194 // Update Full Report tab elements for principal and interest payments 195 $container.find("#out-full-report-principal-payments").text("$" + wwd_moneyFormat(result.loanAmount, 0)); 196 $container.find("#out-full-report-interest-payments").text("$" + wwd_moneyFormat(result.totalInterestWithExtra, 0)); 197 198 // Update amortization chart 199 updateAmortizationChart(result, $container.find("#out-payoff-amortization-chart")); 200 201 // Update principal vs interest chart 202 updatePrincipalVsInterestChart(result, $container.find("#out-payoff-principal-vs-interest-chart")); 203 204 // Update amortization schedule 205 updateAmortizationSchedule(result, $container.find("#out-payoff-amortization-schedule-table")); 206 207 // Populate PDF input details 208 populatePDFInputDetails(); 209 } 210 211 // Function to populate PDF input details 212 function populatePDFInputDetails() { 213 const pdfInputDetails = $container.find("#pdf-input-details-grid"); 214 215 if (pdfInputDetails.length === 0) { 216 console.log("PDF Input Details: grid element not found"); 217 return; 218 } 219 220 // Convert loan type to display format 221 const loanTypeMap = { 222 new: "New Loan", 223 existing: "Existing Loan", 224 }; 225 226 // Convert extra payment frequency to display format 227 const frequencyMap = { 228 monthly: "Monthly", 229 annually: "Annually", 230 one_time: "One Time", 231 }; 232 233 const inputDetails = [ 234 { label: "Loan Type", value: loanTypeMap[loanType] || loanType }, 235 { label: "Current Mortgage Balance", value: `$${wwd_moneyFormat(currentMortgageBalance, 0)}` }, 236 { label: "Interest Rate", value: `${wwd_moneyFormat(interestRate * 100, 2)}%` }, 237 { label: "Loan Term", value: `${loanTerm} years` }, 238 { label: "Extra Payment Amount", value: `$${wwd_moneyFormat(extraPaymentAmount, 0)}` }, 239 { label: "Extra Payment Frequency", value: frequencyMap[extraPaymentFrequency] || extraPaymentFrequency }, 240 ]; 241 242 // Add conditional fields based on loan type 243 if (loanType === "existing") { 244 inputDetails.splice(2, 0, { label: "Years Left", value: `${yearsLeft} years` }); 245 inputDetails.splice(3, 0, { label: "Months Left", value: `${monthsLeft} months` }); 246 } 247 248 let html = "";
249 inputDetails.forEach((detail) => { 250 html += ` 251 <div> 252 <span class="block text-sm text-gray-600 mb-1">${detail.label}</span> 253 <span class="block text-lg font-bold">${detail.value}</span> 254 </div> 255 `; 256 }); 257 258 pdfInputDetails.html(html); 259 console.log("PDF Input Details: populated successfully"); 260 } 261 262 function updateInterestOverTimeChart(result, $container) { 263 const ctx = $container.get(0).getContext("2d"); 264 265 // Generate data for 30 years 266 const months = result.amortizationSchedule.map((entry) => entry.month); 267 const interestPaid = result.amortizationSchedule.map((entry) => entry.totalInterestPaid); 268 const interestPaidWithExtra = result.amortizationSchedule.map((entry) => entry.totalInterestPaidWithExtra); 269 const totalSaved = result.amortizationSchedule.map((entry) => entry.totalSaved); 270 const paidOffMonth = result.loanTermMonthsWithExtra; 271 272 const maxValue = Math.max(...interestPaid, ...interestPaidWithExtra); 273 const tickSpacing = Math.pow(10, Math.floor(Math.log10(maxValue)) - 1); 274 const roundedMaxValue = Math.ceil(maxValue / tickSpacing) * tickSpacing; 275 const midValue = roundedMaxValue / 2;
276 let midPointTicked = false; 277 let midPointLineDrawn = false; 278 279 const breakEvenPlugin = { 280 id: "breakEvenLine", 281 afterDraw: (chart, args, options) => { 282 const { 283 ctx, 284 chartArea: { top, bottom, left, right, width, height }, 285 scales: { x, y }, 286 } = chart; 287 const breakEvenMonth = paidOffMonth; 288 289 ctx.save(); 290 291 // Draw vertical line 292 const xPos = x.getPixelForValue(breakEvenMonth); 293 294 ctx.strokeStyle = "rgba(0, 0, 0, 0.1)"; 295 ctx.setLineDash([5, 5]); 296 ctx.beginPath(); 297 ctx.moveTo(xPos, top + 5); 298 ctx.lineTo(xPos, bottom); 299 ctx.stroke(); 300 301 ctx.font = "14px var(--wwd-font-family)"; 302 ctx.fillStyle = "rgba(0, 0, 0, 0.5)"; 303 ctx.textAlign = "center"; 304 // ctx.fillText("EXTRA PAYMENTS:", xPos, top + 20); 305 // ctx.fillText("MORTGAGE PAID OFF", xPos, top + 40); 306 307 ctx.restore(); 308 }, 309 }; 310 311 let chart = Chart.getChart(ctx); 312 313 if (chart) { 314 // Update existing chart 315 chart.data.labels = months; 316 chart.data.datasets[0].data = interestPaid; 317 chart.data.datasets[1].data = interestPaidWithExtra; 318 chart.data.datasets[2].data = totalSaved; 319 chart.options.plugins.breakEvenLine = { paidOffMonth }; 320 321 // Recalculate y-axis scale 322 const maxValue = Math.max(...interestPaid, ...interestPaidWithExtra); 323 const tickSpacing = Math.pow(10, Math.floor(Math.log10(maxValue)) - 1); 324 const roundedMaxValue = Math.ceil(maxValue / tickSpacing) * tickSpacing; 325 const midValue = roundedMaxValue / 2; 326 327 // Update y-axis configuration 328 chart.options.scales.y.max = roundedMaxValue; 329 chart.options.scales.y.ticks.callback = function (value, index, values) { 330 if (value === 0 || value === roundedMaxValue || (value > midValue && !this.midPointTicked)) {
331 this.midPointTicked = value > midValue; 332 if (value >= 1000) { 333 return "$" + (value / 1000).toFixed(0) + "K"; 334 } else { 335 return "$" + value.toLocaleString(); 336 } 337 } 338 return ""; 339 }; 340 341 chart.options.scales.y.grid.color = function (context) { 342 if (context.tick.value === roundedMaxValue || (context.tick.value > midValue && !this.midPointLineDrawn)) { 343 this.midPointLineDrawn = context.tick.value > midValue; 344 return "rgba(0, 0, 0, 0.1)"; 345 } 346 return "rgba(0, 0, 0, 0)"; 347 }; 348 349 // Reset the midPointTicked and midPointLineDrawn flags 350 chart.options.scales.y.ticks.midPointTicked = false; 351 chart.options.scales.y.grid.midPointLineDrawn = false; 352 353 chart.update(); 354 } else { 355 chart = new Chart(ctx, { 356 type: "line", 357 data: { 358 labels: months, 359 datasets: [ 360 { 361 label: "Interest Paid", 362 data: interestPaid, 363 borderColor: WWD_Calculators.colors.payoff_calc_regular_payment_color, 364 backgroundColor: WWD_Calculators.colors.payoff_calc_regular_payment_color, 365 pointRadius: 0, 366 borderWidth: 2, 367 }, 368 { 369 label: "With Extra Payment", 370 data: interestPaidWithExtra, 371 borderColor: WWD_Calculators.colors.payoff_calc_extra_payment_color, 372 backgroundColor: WWD_Calculators.colors.payoff_calc_extra_payment_color, 373 pointRadius: 0, 374 borderWidth: 2, 375 }, 376 { 377 label: "Total Saved", 378 data: totalSaved, 379 borderColor: WWD_Calculators.colors.payoff_calc_total_saved_color, 380 backgroundColor: WWD_Calculators.colors.payoff_calc_total_saved_color, 381 pointRadius: 0, 382 borderWidth: 2, 383 }, 384 ], 385 }, 386 options: { 387 responsive: true, 388 maintainAspectRatio: false, 389 scales: { 390 x: { 391 title: { 392 display: false, 393 text: "YEAR", 394 }, 395 ticks: { 396 callback: function (value, index, values) { 397 if (value % 2 === 0 && value !== 0) { 398 const currentDate = new Date(); 399 const futureDate = new Date(currentDate.setMonth(currentDate.getMonth() + value)); 400 return futureDate.getFullYear(); 401 } 402 return ""; 403 }, 404 }, 405 grid: { 406 display: true, 407 drawBorder: true, 408 drawOnChartArea: true, 409 drawTicks: true, 410 color: function (context) { 411 return "rgba(0, 0, 0, 0)"; 412 }, 413 }, 414 }, 415 y: { 416 title: { 417 display: false, 418 text: "INTEREST PAID", 419 }, 420 ticks: { 421 callback: function (value, index, values) { 422 if (value === 0 || value === roundedMaxValue || (value > midValue && !midPointTicked)) {
423 midPointTicked = value > midValue; 424 if (value >= 1000) { 425 return "$" + (value / 1000).toFixed(0) + "K"; 426 } else { 427 return "$" + value.toLocaleString(); 428 } 429 } 430 return ""; 431 }, 432 }, 433 min: 0, 434 max: roundedMaxValue, 435 grid: { 436 drawBorder: false, 437 color: function (context) { 438 if (context.tick.value === roundedMaxValue || (context.tick.value > midValue && !midPointLineDrawn)) { 439 midPointLineDrawn = context.tick.value > midValue; 440 return "rgba(0, 0, 0, 0.1)"; 441 } 442 return "rgba(0, 0, 0, 0)"; 443 }, 444 }, 445 border: { 446 display: false, 447 }, 448 }, 449 }, 450 plugins: { 451 tooltip: { 452 enabled: false, // Disable default tooltip 453 external: customTooltip, // Use our custom tooltip 454 mode: "index", 455 intersect: false, 456 }, 457 legend: { 458 display: true, 459 labels: { 460 color: "rgb(255, 99, 132, 0)", 461 boxWidth: 0, 462 }, 463 }, 464 breakEvenLine: {}, 465 }, 466 onHover: (event, chartElement) => { 467 if (chartElement.length) { 468 const datasetIndex = chartElement[0].datasetIndex; 469 const index = chartElement[0].index; 470 updateChartTooltip(index, datasetIndex); 471 } 472 }, 473 }, 474 plugins: [ 475 breakEvenPlugin, 476 { 477 id: "tooltipLine", 478 beforeDraw: (chart) => { 479 if (chart.tooltip._active && chart.tooltip._active.length) { 480 const activePoint = chart.tooltip._active[0]; 481 const { ctx } = chart; 482 const { x } = activePoint.element; 483 const topY = chart.scales.y.top; 484 const bottomY = chart.scales.y.bottom; 485 486 // Draw vertical line 487 ctx.save(); 488 ctx.beginPath(); 489 ctx.moveTo(x, topY); 490 ctx.lineTo(x, bottomY); 491 ctx.lineWidth = 1; 492 ctx.strokeStyle = "rgba(0, 0, 0, 0.5)"; 493 ctx.stroke(); 494 ctx.restore(); 495 } 496 }, 497 }, 498 { 499 id: "customHover", 500 beforeEvent: (chart, args, pluginOptions) => { 501 const event = args.event; 502 if (event.type === "mousemove") { 503 const canvasPosition = Chart.helpers.getRelativePosition(event, chart); 504 const dataX = chart.scales.x.getValueForPixel(canvasPosition.x); 505 if (dataX >= 0 && dataX <= chart.data.labels.length - 1) { 506 updateChartTooltip(Math.round(dataX)); 507 } 508 } 509 }, 510 }, 511 ], 512 }); 513 } 514 515 window.mortgagePayoffChart = chart; 516 517 $container.css("min-height", "300px"); 518 519 function updateChartTooltip(index, datasetIndex = 0) { 520 const dataPoint = chart.data.datasets[datasetIndex].data[index]; 521 const position = chart.getDatasetMeta(datasetIndex).data[index].getCenterPoint(); 522 523 chart.tooltip.setActiveElements( 524 [ 525 { datasetIndex: 0, index: index }, 526 { datasetIndex: 1, index: index }, 527 ], 528 { 529 x: position.x, 530 y: position.y, 531 } 532 ); 533 534 chart.draw(); 535 } 536 537 function customTooltip(context) { 538 // Make sure we have tooltip items 539 if (context.tooltip.dataPoints.length === 0) return; 540 541 const monthsFromNow = parseInt(context.tooltip.dataPoints[0].label) - 1; 542 const currentDate = new Date(); 543 const futureDate = new Date(currentDate.setMonth(currentDate.getMonth() + monthsFromNow)); 544
545 const monthYear = futureDate.toLocaleString("default", { month: "long", year: "numeric" }); 546 const totalInterest = context.tooltip.dataPoints[0].raw; 547 const totalInterestWithExtra = context.tooltip.dataPoints[1].raw; 548 const totalSaved = context.tooltip.dataPoints[2]?.raw || 0; 549 550 // Create tooltip element 551 let tooltipEl = document.getElementById("chartjs-tooltip"); 552 553 if (!tooltipEl) { 554 tooltipEl = document.createElement("div"); 555 tooltipEl.id = "chartjs-tooltip"; 556 tooltipEl.classList.add("chartjs-tooltip"); 557 tooltipEl.innerHTML = "<div class='bg-white shadow-md p-2'></div>"; 558 document.body.appendChild(tooltipEl); 559 } 560 561 // Hide tooltip if mouse is outside the chart 562 const chartArea = chart.chartArea; 563 const mouseX = context.tooltip.x; 564 const mouseY = context.tooltip.y; 565 566 if (mouseX < chartArea.left || mouseX > chartArea.right || mouseY < chartArea.top || mouseY > chartArea.bottom) { 567 tooltipEl.style.opacity = 0; 568 return; 569 } 570 571 // Set content 572 const tableBody = `<div class="text-sm text-gray-700 font-bold mb-1">${monthYear}</div> 573 <div class="flex items-center mb-0.5"> 574 <span class="w-3 h-3 rounded-full bg-[var(--wwd-payoff-calc-regular-payment-color)] mr-2"></span> 575 <span class="text-sm mr-2">Regular Payments</span> 576 <span class="text-sm text-gray-600 font-bold">${new Intl.NumberFormat("en-US", { style: "currency", currency: "USD", minimumFractionDigits: 0, maximumFractionDigits: 0 }).format(totalInterest)}</span> 577 </div> 578 <div class="flex items-center mb-0.5"> 579 <span class="w-3 h-3 rounded-full bg-[var(--wwd-payoff-calc-extra-payment-color)] mr-2"></span> 580 <span class="text-sm mr-2">With Extra Payment</span> 581 <span class="text-sm text-gray-600 font-bold">${new Intl.NumberFormat("en-US", { style: "currency", currency: "USD", minimumFractionDigits: 0, maximumFractionDigits: 0 }).format(totalInterestWithExtra)}</span> 582 </div> 583 <div class="flex items-center"> 584 <span class="w-3 h-3 rounded-full bg-[var(--wwd-payoff-calc-total-saved-color)] mr-2"></span> 585 <span class="text-sm mr-2">Total Saved</span> 586 <span class="text-sm text-gray-600 font-bold">${new Intl.NumberFormat("en-US", { style: "currency", currency: "USD", minimumFractionDigits: 0, maximumFractionDigits: 0 }).format(totalSaved)}</span> 587 </div>`; 588 589 const tableRoot = tooltipEl.querySelector("div"); 590 tableRoot.innerHTML = tableBody; 591 592 // Position tooltip 593 const position = context.chart.canvas.getBoundingClientRect(); 594 const chartWidth = position.width; 595 const cursorX = context.tooltip.caretX; 596 597 if (cursorX > chartWidth / 2) { 598 tooltipEl.style.left = position.left + window.pageXOffset + context.tooltip.caretX - tooltipEl.offsetWidth - 20 + "px"; 599 } else { 600 tooltipEl.style.left = 20 + position.left + window.pageXOffset + context.tooltip.caretX + "px"; 601 } 602 603 tooltipEl.style.opacity = 1; 604 tooltipEl.style.position = "absolute"; 605 tooltipEl.style.top = position.top - 150 + window.pageYOffset + context.tooltip.caretY + "px"; 606 tooltipEl.style.font = context.tooltip.options.bodyFont.string; 607 tooltipEl.style.padding = context.tooltip.options.padding + "px " + context.tooltip.options.padding + "px"; 608 } 609 } 610 611 function updateAmortizationChart(result, $container) { 612 // Destroy existing chart if it exists 613 if (window.amortizationChart) { 614 window.amortizationChart.destroy(); 615 } 616 617 const schedule = result.amortizationSchedule.slice(0, result.loanTermMonthsWithExtra); 618 const labels = schedule.map((entry) => entry.month); 619 const principalData = schedule.map((entry) => entry.totalPrincipalPaidWithExtra); 620 const interestData = schedule.map((entry) => entry.totalInterestPaidWithExtra); 621 const remainingBalanceData = schedule.map((entry) => entry.remainingBalanceWithExtra); 622 623 const principalColor = WWD_Calculators.colors.principal_color; 624 const interestColor = WWD_Calculators.colors.interest_color; 625 const remainingBalanceColor = WWD_Calculators.colors.remaining_balance_color; 626 627 const data = { 628 labels: labels, 629 datasets: [ 630 { 631 label: "Principal", 632 data: principalData, 633 borderColor: principalColor, 634 backgroundColor: principalColor, 635 fill: false, 636 tension: 0.1, 637 }, 638 { 639 label: "Interest", 640 data: interestData, 641 borderColor: interestColor, 642 backgroundColor: interestColor, 643 fill: false, 644 tension: 0.1, 645 }, 646 { 647 label: "Remaining", 648 data: remainingBalanceData, 649 borderColor: remainingBalanceColor, 650 backgroundColor: remainingBalanceColor, 651 fill: false, 652 tension: 0.1, 653 }, 654 ], 655 }; 656 657 const config = { 658 type: "line", 659 data: data, 660 options: { 661 responsive: true, 662 plugins: { 663 legend: { 664 position: "top", 665 }, 666 title: { 667 display: false, 668 text: "Amortization Schedule", 669 }, 670 tooltip: { 671 mode: "index", 672 intersect: false, 673 callbacks: { 674 title: function (context) { 675 return "Month " + context[0].label; 676 }, 677 label: function (context) { 678 let label = context.dataset.label || ""; 679 if (label) { 680 label += ": "; 681 } 682 if (context.parsed.y !== null) { 683 label += new Intl.NumberFormat("en-US", { style: "currency", currency: "USD" }).format(context.parsed.y); 684 } 685 return label; 686 }, 687 }, 688 }, 689 }, 690 scales: { 691 x: { 692 title: { 693 display: true, 694 text: "Months", 695 }, 696 ticks: { 697 callback: function (value, index, values) { 698 return value % 100 === 0 ? value + " mo" : ""; 699 }, 700 }, 701 }, 702 y: { 703 title: { 704 display: true, 705 text: "Amount", 706 }, 707 ticks: { 708 callback: function (value, index, values) {
709 return "$" + value.toLocaleString(); 710 }, 711 }, 712 }, 713 }, 714 elements: { 715 line: { 716 borderWidth: 1.5, // Make lines thinner 717 }, 718 point: { 719 radius: 0, 720 hitRadius: 5, 721 hoverRadius: 3, 722 }, 723 }, 724 }, 725 }; 726 727 window.amortizationChart = new Chart($container[0], config); 728 } 729 730 function updatePrincipalVsInterestChart(result, $container) { 731 // Destroy existing chart if it exists 732 if (window.principalVsInterestChart) { 733 window.principalVsInterestChart.destroy(); 734 } 735 736 const schedule = result.amortizationSchedule.slice(0, result.loanTermMonthsWithExtra); 737 const labels = schedule.map((entry) => entry.month); 738 const principalData = schedule.map((entry) => entry.principalPaymentWithExtra); 739 const interestData = schedule.map((entry) => entry.interestPaymentWithExtra); 740 741 const principalColor = WWD_Calculators.colors.principal_color; 742 const interestColor = WWD_Calculators.colors.interest_color; 743 744 const data = { 745 labels: labels, 746 datasets: [ 747 { 748 label: "Principal", 749 data: principalData, 750 backgroundColor: principalColor, 751 fill: true, 752 }, 753 { 754 label: "Interest", 755 data: interestData, 756 backgroundColor: interestColor, 757 fill: true, 758 }, 759 ], 760 }; 761 762 const config = { 763 type: "line", 764 data: data, 765 options: { 766 responsive: true, 767 plugins: { 768 legend: { 769 position: "top", 770 }, 771 title: { 772 display: false, 773 text: "Principal vs Interest", 774 }, 775 tooltip: { 776 mode: "index", 777 intersect: false, 778 callbacks: { 779 title: function (context) { 780 return "Month " + context[0].label; 781 }, 782 label: function (context) { 783 let label = context.dataset.label || ""; 784 if (label) { 785 label += ": "; 786 } 787 if (context.parsed.y !== null) { 788 label += new Intl.NumberFormat("en-US", { style: "currency", currency: "USD" }).format(context.parsed.y); 789 } 790 return label; 791 }, 792 }, 793 }, 794 }, 795 scales: { 796 x: { 797 stacked: true, 798 title: { 799 display: true, 800 text: "Months", 801 }, 802 ticks: { 803 callback: function (value, index, values) { 804 return value % 100 === 0 ? value + "mo" : ""; 805 }, 806 }, 807 }, 808 y: { 809 stacked: true, 810 title: { 811 display: true, 812 text: "Amount", 813 }, 814 ticks: { 815 callback: function (value, index, values) { 816 return "$" + value.toLocaleString(); 817 }, 818 }, 819 }, 820 }, 821 elements: { 822 line: { 823 tension: 0.4, 824 }, 825 point: { 826 radius: 0, 827 }, 828 }, 829 }, 830 }; 831 832 window.principalVsInterestChart = new Chart($container[0], config); 833 } 834 835 function updateAmortizationSchedule(result, $container) { 836 const schedule = result.amortizationSchedule; 837 const itemsPerPage = 12;
838 let currentPage = 1; 839 840 function renderTable(page) { 841 const startIndex = (page - 1) * itemsPerPage; 842 const endIndex = startIndex + itemsPerPage; 843 const pageItems = schedule.slice(startIndex, endIndex); 844 845 let tableHtml = ` 846 <div class="overflow-x-auto w-full wwd-amortization-table"> 847 <table class="w-full text-sm text-left"> 848 <thead> 849 <tr class="bg-gray-100"> 850 <th class="px-4 py-2 text-[var(--wwd-heading-color)]">Month</th> 851 <th class="px-4 py-2 text-[var(--wwd-heading-color)]">Interest</th> 852 <th class="px-4 py-2 text-[var(--wwd-heading-color)]">Principal</th> 853 <th class="px-4 py-2 text-[var(--wwd-heading-color)]">Extra</th> 854 <th class="px-4 py-2 text-[var(--wwd-heading-color)]">Remaining</th> 855 </tr> 856 </thead> 857 <tbody> 858 `; 859 860 pageItems.forEach((item, index) => { 861 const rowClass = index % 2 === 0 ? "bg-white" : "bg-gray-50"; 862 tableHtml += ` 863 <tr class="${rowClass}"> 864 <td class="px-4 py-2 text-[var(--wwd-body-color)]">${item.month}</td> 865 <td class="px-4 py-2 text-[var(--wwd-body-color)]">$${wwd_moneyFormat(item.interestPaymentWithExtra, 0)}</td> 866 <td class="px-4 py-2 text-[var(--wwd-body-color)]">$${wwd_moneyFormat(item.principalPayment, 0)}</td> 867 <td class="px-4 py-2 text-[var(--wwd-body-color)]">$${wwd_moneyFormat(item.extraPayment, 0)}</td> 868 <td class="px-4 py-2 text-[var(--wwd-body-color)]">$${wwd_moneyFormat(item.remainingBalanceWithExtra, 0)}</td> 869 </tr> 870 `; 871 }); 872 873 tableHtml += ` 874 </tbody> 875 </table> 876 </div> 877 `; 878 879 $container.html(tableHtml); 880 881 // Add pagination 882 const totalPages = Math.ceil(schedule.length / itemsPerPage); 883 let paginationHtml = ` 884 <div class="flex items-center justify-center mt-4 space-x-2"> 885 <button class="pagination-prev px-3 py-1 rounded border ${page === 1 ? "opacity-50 cursor-not-allowed" : ""}"><</button> 886 `; 887 888 for (let i = 1; i <= totalPages; i++) { 889 if (i === 1 || i === totalPages || (i >= page - 2 && i <= page + 2)) { 890 paginationHtml += ` 891 <button class="pagination-page px-1.5 sm:px-3 py-1 rounded border text-xs sm:text-sm ${i === page ? "bg-[var(--wwd-primary-color)] text-white" : ""}">${i}</button> 892 `; 893 } else if (i === page - 3 || i === page + 3) { 894 paginationHtml += `<span class="px-1">...</span>`; 895 } 896 } 897 898 paginationHtml += ` 899 <button class="pagination-next px-3 py-1 rounded border text-xs sm:text-sm ${page === totalPages ? "opacity-50 cursor-not-allowed" : ""}">></button> 900 </div> 901 `; 902 903 $container.append(paginationHtml); 904 905 // Add event listeners for pagination 906 $container.find(".pagination-prev").on("click", function () { 907 if (currentPage > 1) { 908 currentPage--; 909 renderTable(currentPage); 910 } 911 }); 912 913 $container.find(".pagination-next").on("click", function () { 914 if (currentPage < totalPages) { 915 currentPage++; 916 renderTable(currentPage); 917 } 918 }); 919 920 $container.find(".pagination-page").on("click", function () { 921 currentPage = parseInt(jQuery(this).text()); 922 renderTable(currentPage); 923 }); 924 } 925 926 renderTable(currentPage); 927 } 928}; 929 930function initMortgagePayoffCalculator() { 931 var hasMortgagePayoffCalculator = jQuery(".wwd-calculator-form[data-type='mortgage_payoff']").length > 0; 932 933 if (!hasMortgagePayoffCalculator) { 934 return; 935 } 936 937 function updateFieldsVisibility() { 938 const $form = jQuery('.wwd-calculator-form[data-type="mortgage_payoff"]'); 939 const $wrapper = $form.closest(".wwd-calculator-wrapper"); 940 941 const loanType = $form.find('input[name="loan_type"]:checked').val(); 942 943 // Update visibility based on loan type 944 $wrapper.find("[data-loan-type]").each(function () { 945 const $element = jQuery(this); 946 const elementLoanType = $element.attr("data-loan-type"); 947 948 if (elementLoanType === loanType || elementLoanType === undefined) { 949 $element.show(); 950 } else { 951 $element.hide(); 952 } 953 }); 954 955 // Trigger recalculation 956 $form.trigger("submit"); 957 } 958 959 // Add event listeners 960 jQuery(document).ready(function ($) { 961 const $form = $('.wwd-calculator-form[data-type="mortgage_payoff"]'); 962 963 $form.on("change", 'input[name="loan_type"]', function () { 964 updateFieldsVisibility(); 965 }); 966 967 // Initial update 968 updateFieldsVisibility(); 969 }); 970 971 jQuery(document).ready(function ($) { 972 $("#down_payment_percentage").on("input", function () { 973 lastUpdated = "percentage"; 974 updateDownPaymentAmount(); 975 var $range = $('input[data-type="range-input"][data-target="down_payment_percentage"]'); 976 var val = parseFloat($(this).val()); 977 var min = parseFloat($range.attr("min")); 978 var max = parseFloat($range.attr("max")); 979 980 // Calculate and update progress 981 var progress = Math.min(((val - min) / (max - min)) * 100, 100); 982 $range.val(val); 983 $range[0].style.setProperty("--range-progress", `${progress}%`); 984 }); 985 986 $("#down_payment_amount").on("input", function () { 987 lastUpdated = "amount"; 988 updateDownPaymentPercentage(); 989 }); 990 991 $('input[data-type="range-input"][data-target="home_value"]').on("input", function () { 992 $("#home_value") 993 .val(wwd_moneyFormat($(this).val(), 0)) 994 .trigger("input"); 995 }); 996 997 $('input[data-type="range-input"][data-target="down_payment_percentage"]').on("input", function () { 998 $("#down_payment_percentage").val($(this).val()).trigger("input"); 999 }); 1000 1001 $("#interest_rate").on("input", function () { 1002 var $range = $('input[data-type="range-input"][data-target="interest_rate"]'); 1003 var val = parseFloat($(this).val()); 1004 var min = parseFloat($range.attr("min")); 1005 var max = parseFloat($range.attr("max")); 1006 1007 // Calculate and update progress 1008 var progress = Math.min(((val - min) / (max - min)) * 100, 100); 1009 1010 $range.val(val); 1011 $range[0].style.setProperty("--range-progress", `${progress}%`); 1012 }); 1013 1014 // Trigger the change event on page load to set initial state 1015 $('input[name="loan_type"]:checked').trigger("change"); 1016 }); 1017 1018 window.addEventListener("wwdTabSwitch", handleMortgagePayoffCalculatorSubmit); 1019 1020 function handleMortgagePayoffCalculatorSubmit() { 1021 // destroy existing chart if it exists 1022 if (window.mortgagePayoffChart) { 1023 window.mortgagePayoffChart.destroy(); 1024 } 1025 // hide the tooltip if it exists 1026 var tooltip = document.getElementById("chartjs-tooltip"); 1027 if (tooltip) { 1028 tooltip.style.opacity = 0; 1029 } 1030 setTimeout(function () { 1031 window.wwd_submitForms("mortgage_payoff"); 1032 }, 100); 1033 } 1034} 1035 1036// Initialize the conventional mortgage calculator 1037initMortgagePayoffCalculator();
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.