@archerjessop/utilities 7.15.0 → 7.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/browser/financial/tooltip-content-generators.js +1 -1
- package/dist/browser/financial/tooltip-content-generators.js.map +1 -1
- package/dist/browser/ui/click-handlers.js +1 -1
- package/dist/browser/ui/click-handlers.js.map +1 -1
- package/dist/browser/widget/finance.js +1 -1
- package/dist/browser/widget/finance.js.map +1 -1
- package/dist/browser/widget/render.js +1 -1
- package/dist/browser/widget/render.js.map +1 -1
- package/package.json +1 -1
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{calculateDownPaymentTooltip as e}from"./tooltip-calculations.js";function generatePriceTooltipHTML(e){return`\n <strong>Click the price to decrease by 10%</strong><br>\n Current discount: ${e}%\n <hr>\n <em>Click the label to reset</em>\n `}function generateCashFlowTooltipHTML(e,t){const n=12*t;return`\n <strong>Cash Flow Yield:</strong> ${(n/e*100).toFixed(1)}%<br>\n <strong>Annual Cash Flow:</strong> $${n.toLocaleString()}\n `}function generateCapRateTooltipHTML(e){return e?"\n <strong>Click the cap rate to
|
|
1
|
+
import{calculateDownPaymentTooltip as e}from"./tooltip-calculations.js";function generatePriceTooltipHTML(e){return`\n <strong>Click the price to decrease by 10%</strong><br>\n Current discount: ${e}%\n <hr>\n <em>Click the label to reset</em>\n `}function generateCashFlowTooltipHTML(e,t){const n=12*t;return`\n <strong>Cash Flow Yield:</strong> ${(n/e*100).toFixed(1)}%<br>\n <strong>Annual Cash Flow:</strong> $${n.toLocaleString()}\n `}function generateCapRateTooltipHTML(e){return e?"\n <strong>Click the cap rate to enter a value</strong>\n <hr>\n <em>Click the label to reset</em>\n ":null}function generateDownPaymentTooltipHTML(t,n,o,r,l,a="dscr_residential"){return`\n <strong>Click the down payment to decrease by 10%</strong><br>\n ${e(t,n,o,r,l,a)}\n <hr>\n <em>Click the label to reset</em>\n `}export{generateCapRateTooltipHTML,generateCashFlowTooltipHTML,generateDownPaymentTooltipHTML,generatePriceTooltipHTML};
|
|
2
2
|
//# sourceMappingURL=tooltip-content-generators.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tooltip-content-generators.js","sources":["../../../src/browser/financial/tooltip-content-generators.js"],"sourcesContent":["import { calculateDownPaymentTooltip, calculateCashFlowTooltip } from './tooltip-calculations.js';\r\n\r\nexport function generatePriceTooltipHTML(priceDiscount) {\r\n return `\r\n <strong>Click the price to decrease by 10%</strong><br>\r\n Current discount: ${priceDiscount}%\r\n <hr>\r\n <em>Click the label to reset</em>\r\n `;\r\n}\r\n\r\nexport function generateCashFlowTooltipHTML(price, monthlyCashFlow) {\r\n const annualCashFlow = monthlyCashFlow * 12;\r\n const cashFlowYield = ((annualCashFlow / price) * 100).toFixed(1);\r\n\r\n return `\r\n <strong>Cash Flow Yield:</strong> ${cashFlowYield}%<br>\r\n <strong>Annual Cash Flow:</strong> $${annualCashFlow.toLocaleString()}\r\n `;\r\n}\r\n\r\nexport function generateCapRateTooltipHTML(isUsingEstimatedCapRate) {\r\n if (!isUsingEstimatedCapRate) return null;\r\n\r\n return `\r\n <strong>Click the cap rate to
|
|
1
|
+
{"version":3,"file":"tooltip-content-generators.js","sources":["../../../src/browser/financial/tooltip-content-generators.js"],"sourcesContent":["import { calculateDownPaymentTooltip, calculateCashFlowTooltip } from './tooltip-calculations.js';\r\n\r\nexport function generatePriceTooltipHTML(priceDiscount) {\r\n return `\r\n <strong>Click the price to decrease by 10%</strong><br>\r\n Current discount: ${priceDiscount}%\r\n <hr>\r\n <em>Click the label to reset</em>\r\n `;\r\n}\r\n\r\nexport function generateCashFlowTooltipHTML(price, monthlyCashFlow) {\r\n const annualCashFlow = monthlyCashFlow * 12;\r\n const cashFlowYield = ((annualCashFlow / price) * 100).toFixed(1);\r\n\r\n return `\r\n <strong>Cash Flow Yield:</strong> ${cashFlowYield}%<br>\r\n <strong>Annual Cash Flow:</strong> $${annualCashFlow.toLocaleString()}\r\n `;\r\n}\r\n\r\nexport function generateCapRateTooltipHTML(isUsingEstimatedCapRate) {\r\n if (!isUsingEstimatedCapRate) return null;\r\n\r\n return `\r\n <strong>Click the cap rate to enter a value</strong>\r\n <hr>\r\n <em>Click the label to reset</em>\r\n `;\r\n}\r\n\r\nexport function generateDownPaymentTooltipHTML(price, noi, downPercent, dscrPercent, sellerFiPercent, interestRateType = \"dscr_residential\") {\r\n // Use existing calculation function\r\n const cocrText = calculateDownPaymentTooltip(price, noi, downPercent, dscrPercent, sellerFiPercent, interestRateType);\r\n\r\n return `\r\n <strong>Click the down payment to decrease by 10%</strong><br>\r\n ${cocrText}\r\n <hr>\r\n <em>Click the label to reset</em>\r\n `;\r\n}\r\n"],"names":["generatePriceTooltipHTML","priceDiscount","generateCashFlowTooltipHTML","price","monthlyCashFlow","annualCashFlow","toFixed","toLocaleString","generateCapRateTooltipHTML","isUsingEstimatedCapRate","generateDownPaymentTooltipHTML","noi","downPercent","dscrPercent","sellerFiPercent","interestRateType","calculateDownPaymentTooltip"],"mappings":"wEAEO,SAASA,yBAAyBC,GACvC,MAAO,wFAEeA,yDAIxB,CAEO,SAASC,4BAA4BC,EAAOC,GACjD,MAAMC,EAAmC,GAAlBD,EAGvB,MAAO,4CAFiBC,EAAiBF,EAAS,KAAKG,QAAQ,oDAIvBD,EAAeE,sBAEzD,CAEO,SAASC,2BAA2BC,GACzC,OAAKA,EAEE,kHAF8B,IAOvC,CAEO,SAASC,+BAA+BP,EAAOQ,EAAKC,EAAaC,EAAaC,EAAiBC,EAAmB,oBAIvH,MAAO,6EAFUC,EAA4Bb,EAAOQ,EAAKC,EAAaC,EAAaC,EAAiBC,yDAQtG"}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{updateTooltipContent as t,attachTooltip as e,removeTooltip as n}from"./tooltip-manager.js";import{generateCapRateTooltipHTML as
|
|
1
|
+
import{updateTooltipContent as t,attachTooltip as e,removeTooltip as n}from"./tooltip-manager.js";import{generateCapRateTooltipHTML as a,generateDownPaymentTooltipHTML as r,generatePriceTooltipHTML as c}from"../financial/tooltip-content-generators.js";import{FINANCIAL_CONSTANTS as i}from"../../config/financial.js";function updateDiscountButtonText(t){const e=document.getElementById("ln-discount-btn");e&&(e.textContent=t.currentPriceDiscount>0?"Reset to Asking":"85% of Asking")}function setupDiscountButtonHandler(t,e){if(!t)return;if("true"===t.dataset.handlerAttached)return;t.dataset.handlerAttached="true";const{state:n,updateState:a}=e;t.addEventListener("click",function(t){t.preventDefault(),t.stopPropagation(),n.currentPriceDiscount>0?a({currentPriceDiscount:0}):a({currentPriceDiscount:15});const r=document.getElementById("prop-price");r&&(r.textContent=e.getCurrentPrice()),e.updatePriceLabel(),e.recalculateFinancials(),updateDiscountButtonText(n)})}function setupPriceClickHandler(n,a,r){if(!n||!a)return;if("true"===n.dataset.handlerAttached)return;n.dataset.handlerAttached="true";const{state:i,updateState:o}=r,u=n.closest(".metric");function openPriceInput(){if(n.querySelector("input"))return;const t=document.createElement("input");t.type="text",t.value="",t.placeholder="price $",t.className="price-input",t.style.width="110px",n.textContent="",n.appendChild(t),t.focus();let e=!1;const finish=a=>{if(e)return;e=!0;const c=t.value;t.remove(),a?function(t){const e=String(t).match(/[\d,.]+/),a=e?parseFloat(e[0].replace(/,/g,"")):NaN;if(Number.isFinite(a)&&a>0){const t=`$${Math.round(a).toLocaleString()}`;o({baseNOI:null,currentPriceDiscount:0,originalPrice:t,priceWasDefaulted:!1}),n.textContent=t}r.updatePriceLabel(),r.recalculateFinancials(),updateDiscountButtonText(i)}(c):r.recalculateFinancials()};t.addEventListener("keydown",t=>{"Enter"===t.key?(t.preventDefault(),finish(!0)):"Escape"===t.key&&(t.preventDefault(),finish(!1))}),t.addEventListener("blur",()=>finish(!0))}if(n.addEventListener("click",function(e){if(e.preventDefault(),e.stopPropagation(),n.querySelector("input"))return;if(i.priceWasDefaulted||!/\d/.test(n.textContent||""))return void openPriceInput();let a=10*Math.floor(i.currentPriceDiscount/10)+10;a>50&&(a=0),o({currentPriceDiscount:a});const l=r.getCurrentPrice();if(n.textContent=l,r.updatePriceLabel(),r.recalculateFinancials(),updateDiscountButtonText(i),u){const e=c(i.currentPriceDiscount);t(u,e)}}),a.addEventListener("click",function(e){e.preventDefault(),e.stopPropagation(),o({currentPriceDiscount:0});const a=i.originalPrice;if(n.textContent=a,r.updatePriceLabel(),r.recalculateFinancials(),updateDiscountButtonText(i),u){const e=c(i.currentPriceDiscount);t(u,e)}}),u){const t=c(i.currentPriceDiscount);e(u,t),a.classList.add("has-tooltip")}n.style.cursor="pointer",a.style.cursor="pointer"}function setupCapRateClickHandler(n,r,c){if(!n||!r)return;if("true"===n.dataset.handlerAttached)return;n.dataset.handlerAttached="true";const{state:o,updateState:u}=c,l=n.closest(".metric");if(n.addEventListener("click",function(e){if(e.preventDefault(),e.stopPropagation(),n.querySelector("input"))return;const r=(n.textContent||"").match(/[\d.]+/),i=document.createElement("input");i.type="text",i.value=r?r[0]:"",i.placeholder="cap %",i.className="cap-input",i.style.width="56px",n.textContent="",n.appendChild(i),i.focus(),i.select();let s=!1;const finish=e=>{if(s)return;s=!0;const n=i.value;i.remove(),e?function(e){const n=String(e).match(/[\d.]+/),r=n?parseFloat(n[0]):NaN;if(Number.isFinite(r)&&r>0&&u({baseNOI:null,capManuallySet:!0,currentEstimatedCapRate:r,isUsingEstimatedCapRate:!0}),c.recalculateFinancials(),l){const e=a(o.isUsingEstimatedCapRate);e&&t(l,e)}}(n):c.recalculateFinancials()};i.addEventListener("keydown",t=>{"Enter"===t.key?(t.preventDefault(),finish(!0)):"Escape"===t.key&&(t.preventDefault(),finish(!1))}),i.addEventListener("blur",()=>finish(!0))}),r.addEventListener("click",function(e){e.preventDefault(),e.stopPropagation();const r=o.originalCapRate&&!o.originalCapRate.includes("*")?o.originalCapRate.match(/[\d.]+/):null;if(r)n.textContent=`${parseFloat(r[0])}%`,u({baseNOI:null,capManuallySet:!1,isUsingEstimatedCapRate:!1});else{const t=o.originalEstimatedCapRate||100*i.DEFAULT_CAP_RATE;u({baseNOI:null,capManuallySet:!1,currentEstimatedCapRate:t,isUsingEstimatedCapRate:!0})}if(c.recalculateFinancials(),l){const e=a(o.isUsingEstimatedCapRate);e&&t(l,e)}}),l){const t=a(o.isUsingEstimatedCapRate);t&&(e(l,t),r.classList.add("has-tooltip"))}n.style.cursor="pointer",r.style.cursor="pointer"}function setupNoiClickHandler(t,e,n){if(!t||!e)return;if("true"===t.dataset.handlerAttached)return;t.dataset.handlerAttached="true";const{state:a,updateState:r}=n;t.addEventListener("click",function(e){if(e.preventDefault(),e.stopPropagation(),"str"!==a.currentPropertyType)return;if(t.querySelector("input"))return;const c=a.cachedStrValue&&Number.isFinite(a.cachedStrValue.value)?String(a.cachedStrValue.value):"",i=document.createElement("input");i.type="text",i.value=c,i.placeholder="Awning gross $/yr",i.className="noi-input",i.style.width="92px",t.textContent="",t.appendChild(i),i.focus(),i.select();let o=!1;const finish=t=>{o||(o=!0,t?function(t){const e=String(t).match(/[\d,.]+/),a=e?parseFloat(e[0].replace(/,/g,"")):NaN;Number.isFinite(a)&&a>0&&r({cachedStrValue:{value:a,type:"gross"},baseNOI:null,capManuallySet:!1}),n.recalculateFinancials()}(i.value):n.recalculateFinancials())};i.addEventListener("keydown",t=>{"Enter"===t.key?(t.preventDefault(),finish(!0)):"Escape"===t.key&&(t.preventDefault(),finish(!1))}),i.addEventListener("blur",()=>finish(!0))}),e.addEventListener("click",function(t){t.preventDefault(),t.stopPropagation(),"str"===a.currentPropertyType&&(r({cachedStrValue:null,baseNOI:null}),n.recalculateFinancials())}),t.style.cursor="pointer",e.style.cursor="pointer"}function setupAwningLinkHandler(t){t&&"true"!==t.dataset.handlerAttached&&(t.dataset.handlerAttached="true",t.addEventListener("click",function(t){t.preventDefault(),t.stopPropagation();const e=document.getElementById("prop-name")?.textContent?.trim()||"";e&&navigator.clipboard?.writeText&&navigator.clipboard.writeText(e).catch(()=>{}),window.open("https://awning.com/airbnb-calculator","_blank","noopener")}))}function setupDownPaymentClickHandler(e,a,c){if(!e||!a)return;if("true"===e.dataset.handlerAttached)return;e.dataset.handlerAttached="true";const{state:o,updateState:u}=c,l=e.closest(".metric");e.addEventListener("click",function(e){e.preventDefault(),e.stopPropagation();let a=o.currentDownPaymentPercent-10,i=o.currentDSCRPercent-10,s=o.currentSellerFiPercent+10;a<0&&(a=60,i=70,s=40),u({currentDownPaymentPercent:a,currentDSCRPercent:i,currentSellerFiPercent:s}),c.updatePercentageLabels(),c.recalculateFinancials(),setTimeout(()=>{const e=document.getElementById("prop-price"),a=document.getElementById("prop-noi");if(e&&a&&l){const c=e.textContent.match(/[\d,]+/),i=a.textContent.match(/[\d,.]+/);if(c&&i){const e=parseFloat(c[0].replace(/,/g,""));let u=parseFloat(i[0].replace(/,/g,""));a.textContent.includes("K")&&(u*=1e3),a.textContent.includes("M")&&(u*=1e6),n(l),setTimeout(()=>{const n=r(e,u,o.currentDownPaymentPercent,o.currentDSCRPercent,o.currentSellerFiPercent,o.currentInterestRateType);t(l,n)},50)}}},100)}),a.addEventListener("click",function(e){e.preventDefault(),e.stopPropagation(),u({currentDownPaymentPercent:100*i.SELLER_FI_DOWN_PAYMENT,currentDSCRPercent:100*i.DEFAULT_DSCR_PERCENTAGE,currentSellerFiPercent:100*i.SELLER_FI_CARRY}),c.updatePercentageLabels(),c.recalculateFinancials(),setTimeout(()=>{const e=document.getElementById("prop-price"),a=document.getElementById("prop-noi");if(e&&a&&l){const c=e.textContent.match(/[\d,]+/),i=a.textContent.match(/[\d,.]+/);if(c&&i){const e=parseFloat(c[0].replace(/,/g,""));let u=parseFloat(i[0].replace(/,/g,""));a.textContent.includes("K")&&(u*=1e3),a.textContent.includes("M")&&(u*=1e6),n(l),setTimeout(()=>{const n=r(e,u,o.currentDownPaymentPercent,o.currentDSCRPercent,o.currentSellerFiPercent,o.currentInterestRateType);t(l,n)},50)}}},100)}),l&&a&&a.classList.add("has-tooltip"),e.style.cursor="pointer",a.style.cursor="pointer"}export{setupAwningLinkHandler,setupCapRateClickHandler,setupDiscountButtonHandler,setupDownPaymentClickHandler,setupNoiClickHandler,setupPriceClickHandler};
|
|
2
2
|
//# sourceMappingURL=click-handlers.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"click-handlers.js","sources":["../../../src/browser/ui/click-handlers.js"],"sourcesContent":["import { attachTooltip, removeTooltip, updateTooltipContent } from './tooltip-manager.js';\r\nimport { generatePriceTooltipHTML, generateCapRateTooltipHTML, generateDownPaymentTooltipHTML } from '../financial/tooltip-content-generators.js';\r\nimport { FINANCIAL_CONSTANTS } from '../../config/financial.js';\r\n\r\n// State is injected via the `callbacks` object (callbacks.state / callbacks.updateState)\r\n// so this shared module has no dependency on any per-platform global-state singleton.\r\n\r\nfunction updateDiscountButtonText(state) {\r\n const btn = document.getElementById(\"ln-discount-btn\");\r\n if (!btn) return;\r\n btn.textContent = state.currentPriceDiscount > 0 ? \"Reset to Asking\" : \"85% of Asking\";\r\n}\r\n\r\nexport function setupDiscountButtonHandler(buttonElement, callbacks) {\r\n if (!buttonElement) return;\r\n\r\n if (buttonElement.dataset.handlerAttached === 'true') return;\r\n buttonElement.dataset.handlerAttached = 'true';\r\n\r\n const { state, updateState } = callbacks;\r\n\r\n buttonElement.addEventListener(\"click\", function(e) {\r\n e.preventDefault();\r\n e.stopPropagation();\r\n\r\n if (state.currentPriceDiscount > 0) {\r\n updateState({ currentPriceDiscount: 0 });\r\n } else {\r\n updateState({ currentPriceDiscount: 15 });\r\n }\r\n\r\n const priceElement = document.getElementById(\"prop-price\");\r\n if (priceElement) {\r\n priceElement.textContent = callbacks.getCurrentPrice();\r\n }\r\n callbacks.updatePriceLabel();\r\n callbacks.recalculateFinancials();\r\n updateDiscountButtonText(state);\r\n });\r\n}\r\n\r\nexport function setupPriceClickHandler(priceElement, priceLabelElement, callbacks) {\r\n if (!priceElement || !priceLabelElement) return;\r\n\r\n // Prevent duplicate attachment\r\n if (priceElement.dataset.handlerAttached === 'true') return;\r\n priceElement.dataset.handlerAttached = 'true';\r\n\r\n const { state, updateState } = callbacks;\r\n const metric = priceElement.closest('.metric');\r\n\r\n priceElement.addEventListener(\"click\", function(e) {\r\n e.preventDefault();\r\n e.stopPropagation();\r\n\r\n let newDiscount = Math.floor(state.currentPriceDiscount / 10) * 10 + 10;\r\n if (newDiscount > 50) {\r\n newDiscount = 0;\r\n }\r\n\r\n updateState({ currentPriceDiscount: newDiscount });\r\n\r\n const newPrice = callbacks.getCurrentPrice();\r\n priceElement.textContent = newPrice;\r\n callbacks.updatePriceLabel();\r\n callbacks.recalculateFinancials();\r\n updateDiscountButtonText(state);\r\n\r\n if (metric) {\r\n const tooltipContent = generatePriceTooltipHTML(state.currentPriceDiscount);\r\n updateTooltipContent(metric, tooltipContent);\r\n }\r\n });\r\n\r\n priceLabelElement.addEventListener(\"click\", function(e) {\r\n e.preventDefault();\r\n e.stopPropagation();\r\n\r\n updateState({ currentPriceDiscount: 0 });\r\n\r\n const resetPrice = state.originalPrice;\r\n priceElement.textContent = resetPrice;\r\n callbacks.updatePriceLabel();\r\n callbacks.recalculateFinancials();\r\n updateDiscountButtonText(state);\r\n\r\n if (metric) {\r\n const tooltipContent = generatePriceTooltipHTML(state.currentPriceDiscount);\r\n updateTooltipContent(metric, tooltipContent);\r\n }\r\n });\r\n\r\n if (metric) {\r\n const tooltipContent = generatePriceTooltipHTML(state.currentPriceDiscount);\r\n attachTooltip(metric, tooltipContent);\r\n priceLabelElement.classList.add('has-tooltip');\r\n }\r\n\r\n priceElement.style.cursor = \"pointer\";\r\n priceLabelElement.style.cursor = \"pointer\";\r\n}\r\n\r\nexport function setupCapRateClickHandler(capElement, capLabelElement, callbacks) {\r\n if (!capElement || !capLabelElement) return;\r\n\r\n const { state, updateState } = callbacks;\r\n if (!state.isUsingEstimatedCapRate) return;\r\n\r\n // Prevent duplicate attachment\r\n if (capElement.dataset.handlerAttached === 'true') return;\r\n capElement.dataset.handlerAttached = 'true';\r\n\r\n const metric = capElement.closest('.metric');\r\n\r\n capElement.addEventListener(\"click\", function(e) {\r\n e.preventDefault();\r\n e.stopPropagation();\r\n\r\n let newCapRate = state.currentEstimatedCapRate + 1;\r\n if (newCapRate > 20) {\r\n newCapRate = 5;\r\n }\r\n\r\n updateState({ currentEstimatedCapRate: newCapRate, capManuallySet: true });\r\n\r\n capElement.textContent = `${newCapRate}%*`;\r\n callbacks.recalculateFinancials();\r\n\r\n if (metric) {\r\n const tooltipContent = generateCapRateTooltipHTML(state.isUsingEstimatedCapRate);\r\n if (tooltipContent) {\r\n updateTooltipContent(metric, tooltipContent);\r\n }\r\n }\r\n });\r\n\r\n capLabelElement.addEventListener(\"click\", function(e) {\r\n e.preventDefault();\r\n e.stopPropagation();\r\n\r\n const originalCapRate = state.originalEstimatedCapRate || FINANCIAL_CONSTANTS.DEFAULT_CAP_RATE;\r\n updateState({ currentEstimatedCapRate: originalCapRate, capManuallySet: false, baseNOI: null });\r\n\r\n capElement.textContent = `${originalCapRate}%*`;\r\n callbacks.recalculateFinancials();\r\n\r\n if (metric) {\r\n const tooltipContent = generateCapRateTooltipHTML(state.isUsingEstimatedCapRate);\r\n if (tooltipContent) {\r\n updateTooltipContent(metric, tooltipContent);\r\n }\r\n }\r\n });\r\n\r\n if (metric) {\r\n const tooltipContent = generateCapRateTooltipHTML(state.isUsingEstimatedCapRate);\r\n if (tooltipContent) {\r\n attachTooltip(metric, tooltipContent);\r\n capLabelElement.classList.add('has-tooltip');\r\n }\r\n }\r\n\r\n capElement.style.cursor = \"pointer\";\r\n capLabelElement.style.cursor = \"pointer\";\r\n}\r\n\r\n// Manual STR-gross entry on the NOI cell (STR mode only). Clicking the NOI value swaps in an\r\n// inline input; committing a positive number stores it as the measured STR gross\r\n// (cachedStrValue {value, type:\"gross\"}) — the SAME seam the dormant str-revenue backend would\r\n// fill — so calculateFinancials applies NOI = gross x NOI_PERCENTAGE. baseNOI is cleared so the\r\n// type model recomputes, and capManuallySet is cleared so a prior cap-click override does not\r\n// clobber the gross. Clicking the NOI label resets to the 5.5%-of-price estimate.\r\nexport function setupNoiClickHandler(noiElement, noiLabelElement, callbacks) {\r\n if (!noiElement || !noiLabelElement) return;\r\n\r\n if (noiElement.dataset.handlerAttached === \"true\") return;\r\n noiElement.dataset.handlerAttached = \"true\";\r\n\r\n const { state, updateState } = callbacks;\r\n\r\n function commit(raw) {\r\n const match = String(raw).match(/[\\d,.]+/);\r\n const value = match ? parseFloat(match[0].replace(/,/g, \"\")) : NaN;\r\n if (Number.isFinite(value) && value > 0) {\r\n updateState({ cachedStrValue: { value, type: \"gross\" }, baseNOI: null, capManuallySet: false });\r\n }\r\n callbacks.recalculateFinancials();\r\n }\r\n\r\n noiElement.addEventListener(\"click\", function(e) {\r\n e.preventDefault();\r\n e.stopPropagation();\r\n\r\n if (state.currentPropertyType !== \"str\") return;\r\n if (noiElement.querySelector(\"input\")) return;\r\n\r\n const current = state.cachedStrValue && Number.isFinite(state.cachedStrValue.value)\r\n ? String(state.cachedStrValue.value)\r\n : \"\";\r\n const input = document.createElement(\"input\");\r\n input.type = \"text\";\r\n input.value = current;\r\n input.placeholder = \"Awning gross $/yr\";\r\n input.className = \"noi-input\";\r\n input.style.width = \"92px\";\r\n noiElement.textContent = \"\";\r\n noiElement.appendChild(input);\r\n input.focus();\r\n input.select();\r\n\r\n let done = false;\r\n const finish = (save) => {\r\n if (done) return;\r\n done = true;\r\n if (save) commit(input.value);\r\n else callbacks.recalculateFinancials();\r\n };\r\n input.addEventListener(\"keydown\", (ev) => {\r\n if (ev.key === \"Enter\") { ev.preventDefault(); finish(true); }\r\n else if (ev.key === \"Escape\") { ev.preventDefault(); finish(false); }\r\n });\r\n input.addEventListener(\"blur\", () => finish(true));\r\n });\r\n\r\n noiLabelElement.addEventListener(\"click\", function(e) {\r\n e.preventDefault();\r\n e.stopPropagation();\r\n\r\n if (state.currentPropertyType !== \"str\") return;\r\n updateState({ cachedStrValue: null, baseNOI: null });\r\n callbacks.recalculateFinancials();\r\n });\r\n\r\n noiElement.style.cursor = \"pointer\";\r\n noiLabelElement.style.cursor = \"pointer\";\r\n}\r\n\r\n// The \"↗ Awning\" affordance next to NOI: copy the current address to the clipboard and open\r\n// Awning's public calculator in a new tab, so the analyst pastes the address, reads the gross\r\n// revenue, and types it back into the NOI cell (setupNoiClickHandler). Read the address from\r\n// the live #prop-name so SPA navigation can't bind a stale value.\r\nexport function setupAwningLinkHandler(linkElement) {\r\n if (!linkElement) return;\r\n\r\n if (linkElement.dataset.handlerAttached === \"true\") return;\r\n linkElement.dataset.handlerAttached = \"true\";\r\n\r\n linkElement.addEventListener(\"click\", function(e) {\r\n e.preventDefault();\r\n e.stopPropagation();\r\n\r\n const address = document.getElementById(\"prop-name\")?.textContent?.trim() || \"\";\r\n if (address && navigator.clipboard?.writeText) {\r\n navigator.clipboard.writeText(address).catch(() => {});\r\n }\r\n window.open(\"https://awning.com/airbnb-calculator\", \"_blank\", \"noopener\");\r\n });\r\n}\r\n\r\nexport function setupDownPaymentClickHandler(downElement, downLabelElement, callbacks) {\r\n if (!downElement || !downLabelElement) return;\r\n\r\n // Prevent duplicate attachment\r\n if (downElement.dataset.handlerAttached === 'true') return;\r\n downElement.dataset.handlerAttached = 'true';\r\n\r\n const { state, updateState } = callbacks;\r\n const metric = downElement.closest('.metric');\r\n\r\n downElement.addEventListener(\"click\", function(e) {\r\n e.preventDefault();\r\n e.stopPropagation();\r\n\r\n let newDownPercent = state.currentDownPaymentPercent - 10;\r\n let newDSCRPercent = state.currentDSCRPercent - 10;\r\n let newSellerFiPercent = state.currentSellerFiPercent + 10;\r\n\r\n if (newDownPercent < 0) {\r\n newDownPercent = 60;\r\n newDSCRPercent = 70;\r\n newSellerFiPercent = 40;\r\n }\r\n\r\n updateState({\r\n currentDownPaymentPercent: newDownPercent,\r\n currentDSCRPercent: newDSCRPercent,\r\n currentSellerFiPercent: newSellerFiPercent\r\n });\r\n\r\n callbacks.updatePercentageLabels();\r\n callbacks.recalculateFinancials();\r\n\r\n setTimeout(() => {\r\n const priceElement = document.getElementById(\"prop-price\");\r\n const noiElement = document.getElementById(\"prop-noi\");\r\n\r\n if (priceElement && noiElement && metric) {\r\n const priceMatch = priceElement.textContent.match(/[\\d,]+/);\r\n const noiMatch = noiElement.textContent.match(/[\\d,.]+/);\r\n\r\n if (priceMatch && noiMatch) {\r\n const price = parseFloat(priceMatch[0].replace(/,/g, \"\"));\r\n let noi = parseFloat(noiMatch[0].replace(/,/g, \"\"));\r\n\r\n if (noiElement.textContent.includes(\"K\")) noi *= 1000;\r\n if (noiElement.textContent.includes(\"M\")) noi *= 1000000;\r\n\r\n removeTooltip(metric);\r\n setTimeout(() => {\r\n const tooltipContent = generateDownPaymentTooltipHTML(\r\n price,\r\n noi,\r\n state.currentDownPaymentPercent,\r\n state.currentDSCRPercent,\r\n state.currentSellerFiPercent,\r\n state.currentInterestRateType\r\n );\r\n updateTooltipContent(metric, tooltipContent);\r\n }, 50);\r\n }\r\n }\r\n }, 100);\r\n });\r\n\r\n downLabelElement.addEventListener(\"click\", function(e) {\r\n e.preventDefault();\r\n e.stopPropagation();\r\n\r\n updateState({\r\n currentDownPaymentPercent: FINANCIAL_CONSTANTS.SELLER_FI_DOWN_PAYMENT * 100,\r\n currentDSCRPercent: FINANCIAL_CONSTANTS.DEFAULT_DSCR_PERCENTAGE * 100,\r\n currentSellerFiPercent: FINANCIAL_CONSTANTS.SELLER_FI_CARRY * 100\r\n });\r\n\r\n callbacks.updatePercentageLabels();\r\n callbacks.recalculateFinancials();\r\n\r\n setTimeout(() => {\r\n const priceElement = document.getElementById(\"prop-price\");\r\n const noiElement = document.getElementById(\"prop-noi\");\r\n\r\n if (priceElement && noiElement && metric) {\r\n const priceMatch = priceElement.textContent.match(/[\\d,]+/);\r\n const noiMatch = noiElement.textContent.match(/[\\d,.]+/);\r\n\r\n if (priceMatch && noiMatch) {\r\n const price = parseFloat(priceMatch[0].replace(/,/g, \"\"));\r\n let noi = parseFloat(noiMatch[0].replace(/,/g, \"\"));\r\n\r\n if (noiElement.textContent.includes(\"K\")) noi *= 1000;\r\n if (noiElement.textContent.includes(\"M\")) noi *= 1000000;\r\n\r\n removeTooltip(metric);\r\n setTimeout(() => {\r\n const tooltipContent = generateDownPaymentTooltipHTML(\r\n price,\r\n noi,\r\n state.currentDownPaymentPercent,\r\n state.currentDSCRPercent,\r\n state.currentSellerFiPercent,\r\n state.currentInterestRateType\r\n );\r\n updateTooltipContent(metric, tooltipContent);\r\n }, 50);\r\n }\r\n }\r\n }, 100);\r\n });\r\n\r\n if (metric && downLabelElement) {\r\n downLabelElement.classList.add('has-tooltip');\r\n }\r\n\r\n downElement.style.cursor = \"pointer\";\r\n downLabelElement.style.cursor = \"pointer\";\r\n}\r\n"],"names":["updateDiscountButtonText","state","btn","document","getElementById","textContent","currentPriceDiscount","setupDiscountButtonHandler","buttonElement","callbacks","dataset","handlerAttached","updateState","addEventListener","e","preventDefault","stopPropagation","priceElement","getCurrentPrice","updatePriceLabel","recalculateFinancials","setupPriceClickHandler","priceLabelElement","metric","closest","newDiscount","Math","floor","newPrice","tooltipContent","generatePriceTooltipHTML","updateTooltipContent","resetPrice","originalPrice","attachTooltip","classList","add","style","cursor","setupCapRateClickHandler","capElement","capLabelElement","isUsingEstimatedCapRate","newCapRate","currentEstimatedCapRate","capManuallySet","generateCapRateTooltipHTML","originalCapRate","originalEstimatedCapRate","FINANCIAL_CONSTANTS","DEFAULT_CAP_RATE","baseNOI","setupNoiClickHandler","noiElement","noiLabelElement","currentPropertyType","querySelector","current","cachedStrValue","Number","isFinite","value","String","input","createElement","type","placeholder","className","width","appendChild","focus","select","done","finish","save","raw","match","parseFloat","replace","NaN","commit","ev","key","setupAwningLinkHandler","linkElement","address","trim","navigator","clipboard","writeText","catch","window","open","setupDownPaymentClickHandler","downElement","downLabelElement","newDownPercent","currentDownPaymentPercent","newDSCRPercent","currentDSCRPercent","newSellerFiPercent","currentSellerFiPercent","updatePercentageLabels","setTimeout","priceMatch","noiMatch","price","noi","includes","removeTooltip","generateDownPaymentTooltipHTML","currentInterestRateType","SELLER_FI_DOWN_PAYMENT","DEFAULT_DSCR_PERCENTAGE","SELLER_FI_CARRY"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"click-handlers.js","sources":["../../../src/browser/ui/click-handlers.js"],"sourcesContent":["import { attachTooltip, removeTooltip, updateTooltipContent } from './tooltip-manager.js';\r\nimport { generatePriceTooltipHTML, generateCapRateTooltipHTML, generateDownPaymentTooltipHTML } from '../financial/tooltip-content-generators.js';\r\nimport { FINANCIAL_CONSTANTS } from '../../config/financial.js';\r\n\r\n// State is injected via the `callbacks` object (callbacks.state / callbacks.updateState)\r\n// so this shared module has no dependency on any per-platform global-state singleton.\r\n\r\nfunction updateDiscountButtonText(state) {\r\n const btn = document.getElementById(\"ln-discount-btn\");\r\n if (!btn) return;\r\n btn.textContent = state.currentPriceDiscount > 0 ? \"Reset to Asking\" : \"85% of Asking\";\r\n}\r\n\r\nexport function setupDiscountButtonHandler(buttonElement, callbacks) {\r\n if (!buttonElement) return;\r\n\r\n if (buttonElement.dataset.handlerAttached === 'true') return;\r\n buttonElement.dataset.handlerAttached = 'true';\r\n\r\n const { state, updateState } = callbacks;\r\n\r\n buttonElement.addEventListener(\"click\", function(e) {\r\n e.preventDefault();\r\n e.stopPropagation();\r\n\r\n if (state.currentPriceDiscount > 0) {\r\n updateState({ currentPriceDiscount: 0 });\r\n } else {\r\n updateState({ currentPriceDiscount: 15 });\r\n }\r\n\r\n const priceElement = document.getElementById(\"prop-price\");\r\n if (priceElement) {\r\n priceElement.textContent = callbacks.getCurrentPrice();\r\n }\r\n callbacks.updatePriceLabel();\r\n callbacks.recalculateFinancials();\r\n updateDiscountButtonText(state);\r\n });\r\n}\r\n\r\nexport function setupPriceClickHandler(priceElement, priceLabelElement, callbacks) {\r\n if (!priceElement || !priceLabelElement) return;\r\n\r\n // Prevent duplicate attachment\r\n if (priceElement.dataset.handlerAttached === 'true') return;\r\n priceElement.dataset.handlerAttached = 'true';\r\n\r\n const { state, updateState } = callbacks;\r\n const metric = priceElement.closest('.metric');\r\n\r\n // Manual price entry — only when the page exposed no usable price (priceWasDefaulted or a\r\n // non-numeric display like \"No price\"). Committing a positive number sets it as the listing\r\n // price and re-flows everything, clearing the all-N/A state a missing price causes. When a\r\n // real price exists, the click keeps cycling the discount (below).\r\n function commitPrice(raw) {\r\n const match = String(raw).match(/[\\d,.]+/);\r\n const value = match ? parseFloat(match[0].replace(/,/g, \"\")) : NaN;\r\n if (Number.isFinite(value) && value > 0) {\r\n const formatted = `$${Math.round(value).toLocaleString()}`;\r\n updateState({ baseNOI: null, currentPriceDiscount: 0, originalPrice: formatted, priceWasDefaulted: false });\r\n priceElement.textContent = formatted;\r\n }\r\n callbacks.updatePriceLabel();\r\n callbacks.recalculateFinancials();\r\n updateDiscountButtonText(state);\r\n }\r\n\r\n function openPriceInput() {\r\n if (priceElement.querySelector(\"input\")) return;\r\n const input = document.createElement(\"input\");\r\n input.type = \"text\";\r\n input.value = \"\";\r\n input.placeholder = \"price $\";\r\n input.className = \"price-input\";\r\n input.style.width = \"110px\";\r\n priceElement.textContent = \"\";\r\n priceElement.appendChild(input);\r\n input.focus();\r\n\r\n let done = false;\r\n const finish = (save) => {\r\n if (done) return;\r\n done = true;\r\n const value = input.value;\r\n input.remove();\r\n if (save) commitPrice(value);\r\n else callbacks.recalculateFinancials();\r\n };\r\n input.addEventListener(\"keydown\", (ev) => {\r\n if (ev.key === \"Enter\") { ev.preventDefault(); finish(true); }\r\n else if (ev.key === \"Escape\") { ev.preventDefault(); finish(false); }\r\n });\r\n input.addEventListener(\"blur\", () => finish(true));\r\n }\r\n\r\n priceElement.addEventListener(\"click\", function(e) {\r\n e.preventDefault();\r\n e.stopPropagation();\r\n\r\n if (priceElement.querySelector(\"input\")) return;\r\n\r\n const priceMissing = state.priceWasDefaulted || !/\\d/.test(priceElement.textContent || \"\");\r\n if (priceMissing) {\r\n openPriceInput();\r\n return;\r\n }\r\n\r\n let newDiscount = Math.floor(state.currentPriceDiscount / 10) * 10 + 10;\r\n if (newDiscount > 50) {\r\n newDiscount = 0;\r\n }\r\n\r\n updateState({ currentPriceDiscount: newDiscount });\r\n\r\n const newPrice = callbacks.getCurrentPrice();\r\n priceElement.textContent = newPrice;\r\n callbacks.updatePriceLabel();\r\n callbacks.recalculateFinancials();\r\n updateDiscountButtonText(state);\r\n\r\n if (metric) {\r\n const tooltipContent = generatePriceTooltipHTML(state.currentPriceDiscount);\r\n updateTooltipContent(metric, tooltipContent);\r\n }\r\n });\r\n\r\n priceLabelElement.addEventListener(\"click\", function(e) {\r\n e.preventDefault();\r\n e.stopPropagation();\r\n\r\n updateState({ currentPriceDiscount: 0 });\r\n\r\n const resetPrice = state.originalPrice;\r\n priceElement.textContent = resetPrice;\r\n callbacks.updatePriceLabel();\r\n callbacks.recalculateFinancials();\r\n updateDiscountButtonText(state);\r\n\r\n if (metric) {\r\n const tooltipContent = generatePriceTooltipHTML(state.currentPriceDiscount);\r\n updateTooltipContent(metric, tooltipContent);\r\n }\r\n });\r\n\r\n if (metric) {\r\n const tooltipContent = generatePriceTooltipHTML(state.currentPriceDiscount);\r\n attachTooltip(metric, tooltipContent);\r\n priceLabelElement.classList.add('has-tooltip');\r\n }\r\n\r\n priceElement.style.cursor = \"pointer\";\r\n priceLabelElement.style.cursor = \"pointer\";\r\n}\r\n\r\n// Manual cap-rate entry on the cap cell — available for EVERY listing (reported, estimated, or\r\n// none). Clicking the cap value swaps in an inline input; committing a positive number routes\r\n// through the engine's capManuallySet override (NOI = original price x cap for every type), so\r\n// any change re-flows all calculations. baseNOI is cleared so the override recomputes, and\r\n// isUsingEstimatedCapRate is set so the calc reads the typed value from state rather than the\r\n// DOM. Clicking the label resets to the page's reported cap when there was one, else to the\r\n// 5% estimate.\r\nexport function setupCapRateClickHandler(capElement, capLabelElement, callbacks) {\r\n if (!capElement || !capLabelElement) return;\r\n\r\n // Prevent duplicate attachment\r\n if (capElement.dataset.handlerAttached === 'true') return;\r\n capElement.dataset.handlerAttached = 'true';\r\n\r\n const { state, updateState } = callbacks;\r\n const metric = capElement.closest('.metric');\r\n\r\n function commit(raw) {\r\n const match = String(raw).match(/[\\d.]+/);\r\n const value = match ? parseFloat(match[0]) : NaN;\r\n if (Number.isFinite(value) && value > 0) {\r\n updateState({\r\n baseNOI: null,\r\n capManuallySet: true,\r\n currentEstimatedCapRate: value,\r\n isUsingEstimatedCapRate: true,\r\n });\r\n }\r\n callbacks.recalculateFinancials();\r\n if (metric) {\r\n const tooltipContent = generateCapRateTooltipHTML(state.isUsingEstimatedCapRate);\r\n if (tooltipContent) updateTooltipContent(metric, tooltipContent);\r\n }\r\n }\r\n\r\n capElement.addEventListener(\"click\", function(e) {\r\n e.preventDefault();\r\n e.stopPropagation();\r\n\r\n if (capElement.querySelector(\"input\")) return;\r\n\r\n const match = (capElement.textContent || \"\").match(/[\\d.]+/);\r\n const input = document.createElement(\"input\");\r\n input.type = \"text\";\r\n input.value = match ? match[0] : \"\";\r\n input.placeholder = \"cap %\";\r\n input.className = \"cap-input\";\r\n input.style.width = \"56px\";\r\n capElement.textContent = \"\";\r\n capElement.appendChild(input);\r\n input.focus();\r\n input.select();\r\n\r\n let done = false;\r\n const finish = (save) => {\r\n if (done) return;\r\n done = true;\r\n const value = input.value;\r\n // Remove the input before recalc so updateActiveCapDisplay can repaint the cap cell\r\n // (prop-cap is painted only there, never by applyFinancials).\r\n input.remove();\r\n if (save) commit(value);\r\n else callbacks.recalculateFinancials();\r\n };\r\n input.addEventListener(\"keydown\", (ev) => {\r\n if (ev.key === \"Enter\") { ev.preventDefault(); finish(true); }\r\n else if (ev.key === \"Escape\") { ev.preventDefault(); finish(false); }\r\n });\r\n input.addEventListener(\"blur\", () => finish(true));\r\n });\r\n\r\n capLabelElement.addEventListener(\"click\", function(e) {\r\n e.preventDefault();\r\n e.stopPropagation();\r\n\r\n const reportedMatch = state.originalCapRate && !state.originalCapRate.includes(\"*\")\r\n ? state.originalCapRate.match(/[\\d.]+/)\r\n : null;\r\n\r\n if (reportedMatch) {\r\n // Restore the page's reported cap: write it to the cell so the non-estimated calc path\r\n // (which reads the cap cell) recomputes against it, not the prior override.\r\n capElement.textContent = `${parseFloat(reportedMatch[0])}%`;\r\n updateState({ baseNOI: null, capManuallySet: false, isUsingEstimatedCapRate: false });\r\n } else {\r\n const originalCapRate = state.originalEstimatedCapRate || FINANCIAL_CONSTANTS.DEFAULT_CAP_RATE * 100;\r\n updateState({\r\n baseNOI: null,\r\n capManuallySet: false,\r\n currentEstimatedCapRate: originalCapRate,\r\n isUsingEstimatedCapRate: true,\r\n });\r\n }\r\n callbacks.recalculateFinancials();\r\n\r\n if (metric) {\r\n const tooltipContent = generateCapRateTooltipHTML(state.isUsingEstimatedCapRate);\r\n if (tooltipContent) {\r\n updateTooltipContent(metric, tooltipContent);\r\n }\r\n }\r\n });\r\n\r\n if (metric) {\r\n const tooltipContent = generateCapRateTooltipHTML(state.isUsingEstimatedCapRate);\r\n if (tooltipContent) {\r\n attachTooltip(metric, tooltipContent);\r\n capLabelElement.classList.add('has-tooltip');\r\n }\r\n }\r\n\r\n capElement.style.cursor = \"pointer\";\r\n capLabelElement.style.cursor = \"pointer\";\r\n}\r\n\r\n// Manual STR-gross entry on the NOI cell (STR mode only). Clicking the NOI value swaps in an\r\n// inline input; committing a positive number stores it as the measured STR gross\r\n// (cachedStrValue {value, type:\"gross\"}) — the SAME seam the dormant str-revenue backend would\r\n// fill — so calculateFinancials applies NOI = gross x NOI_PERCENTAGE. baseNOI is cleared so the\r\n// type model recomputes, and capManuallySet is cleared so a prior cap-click override does not\r\n// clobber the gross. Clicking the NOI label resets to the 5.5%-of-price estimate.\r\nexport function setupNoiClickHandler(noiElement, noiLabelElement, callbacks) {\r\n if (!noiElement || !noiLabelElement) return;\r\n\r\n if (noiElement.dataset.handlerAttached === \"true\") return;\r\n noiElement.dataset.handlerAttached = \"true\";\r\n\r\n const { state, updateState } = callbacks;\r\n\r\n function commit(raw) {\r\n const match = String(raw).match(/[\\d,.]+/);\r\n const value = match ? parseFloat(match[0].replace(/,/g, \"\")) : NaN;\r\n if (Number.isFinite(value) && value > 0) {\r\n updateState({ cachedStrValue: { value, type: \"gross\" }, baseNOI: null, capManuallySet: false });\r\n }\r\n callbacks.recalculateFinancials();\r\n }\r\n\r\n noiElement.addEventListener(\"click\", function(e) {\r\n e.preventDefault();\r\n e.stopPropagation();\r\n\r\n if (state.currentPropertyType !== \"str\") return;\r\n if (noiElement.querySelector(\"input\")) return;\r\n\r\n const current = state.cachedStrValue && Number.isFinite(state.cachedStrValue.value)\r\n ? String(state.cachedStrValue.value)\r\n : \"\";\r\n const input = document.createElement(\"input\");\r\n input.type = \"text\";\r\n input.value = current;\r\n input.placeholder = \"Awning gross $/yr\";\r\n input.className = \"noi-input\";\r\n input.style.width = \"92px\";\r\n noiElement.textContent = \"\";\r\n noiElement.appendChild(input);\r\n input.focus();\r\n input.select();\r\n\r\n let done = false;\r\n const finish = (save) => {\r\n if (done) return;\r\n done = true;\r\n if (save) commit(input.value);\r\n else callbacks.recalculateFinancials();\r\n };\r\n input.addEventListener(\"keydown\", (ev) => {\r\n if (ev.key === \"Enter\") { ev.preventDefault(); finish(true); }\r\n else if (ev.key === \"Escape\") { ev.preventDefault(); finish(false); }\r\n });\r\n input.addEventListener(\"blur\", () => finish(true));\r\n });\r\n\r\n noiLabelElement.addEventListener(\"click\", function(e) {\r\n e.preventDefault();\r\n e.stopPropagation();\r\n\r\n if (state.currentPropertyType !== \"str\") return;\r\n updateState({ cachedStrValue: null, baseNOI: null });\r\n callbacks.recalculateFinancials();\r\n });\r\n\r\n noiElement.style.cursor = \"pointer\";\r\n noiLabelElement.style.cursor = \"pointer\";\r\n}\r\n\r\n// The \"↗ Awning\" affordance next to NOI: copy the current address to the clipboard and open\r\n// Awning's public calculator in a new tab, so the analyst pastes the address, reads the gross\r\n// revenue, and types it back into the NOI cell (setupNoiClickHandler). Read the address from\r\n// the live #prop-name so SPA navigation can't bind a stale value.\r\nexport function setupAwningLinkHandler(linkElement) {\r\n if (!linkElement) return;\r\n\r\n if (linkElement.dataset.handlerAttached === \"true\") return;\r\n linkElement.dataset.handlerAttached = \"true\";\r\n\r\n linkElement.addEventListener(\"click\", function(e) {\r\n e.preventDefault();\r\n e.stopPropagation();\r\n\r\n const address = document.getElementById(\"prop-name\")?.textContent?.trim() || \"\";\r\n if (address && navigator.clipboard?.writeText) {\r\n navigator.clipboard.writeText(address).catch(() => {});\r\n }\r\n window.open(\"https://awning.com/airbnb-calculator\", \"_blank\", \"noopener\");\r\n });\r\n}\r\n\r\nexport function setupDownPaymentClickHandler(downElement, downLabelElement, callbacks) {\r\n if (!downElement || !downLabelElement) return;\r\n\r\n // Prevent duplicate attachment\r\n if (downElement.dataset.handlerAttached === 'true') return;\r\n downElement.dataset.handlerAttached = 'true';\r\n\r\n const { state, updateState } = callbacks;\r\n const metric = downElement.closest('.metric');\r\n\r\n downElement.addEventListener(\"click\", function(e) {\r\n e.preventDefault();\r\n e.stopPropagation();\r\n\r\n let newDownPercent = state.currentDownPaymentPercent - 10;\r\n let newDSCRPercent = state.currentDSCRPercent - 10;\r\n let newSellerFiPercent = state.currentSellerFiPercent + 10;\r\n\r\n if (newDownPercent < 0) {\r\n newDownPercent = 60;\r\n newDSCRPercent = 70;\r\n newSellerFiPercent = 40;\r\n }\r\n\r\n updateState({\r\n currentDownPaymentPercent: newDownPercent,\r\n currentDSCRPercent: newDSCRPercent,\r\n currentSellerFiPercent: newSellerFiPercent\r\n });\r\n\r\n callbacks.updatePercentageLabels();\r\n callbacks.recalculateFinancials();\r\n\r\n setTimeout(() => {\r\n const priceElement = document.getElementById(\"prop-price\");\r\n const noiElement = document.getElementById(\"prop-noi\");\r\n\r\n if (priceElement && noiElement && metric) {\r\n const priceMatch = priceElement.textContent.match(/[\\d,]+/);\r\n const noiMatch = noiElement.textContent.match(/[\\d,.]+/);\r\n\r\n if (priceMatch && noiMatch) {\r\n const price = parseFloat(priceMatch[0].replace(/,/g, \"\"));\r\n let noi = parseFloat(noiMatch[0].replace(/,/g, \"\"));\r\n\r\n if (noiElement.textContent.includes(\"K\")) noi *= 1000;\r\n if (noiElement.textContent.includes(\"M\")) noi *= 1000000;\r\n\r\n removeTooltip(metric);\r\n setTimeout(() => {\r\n const tooltipContent = generateDownPaymentTooltipHTML(\r\n price,\r\n noi,\r\n state.currentDownPaymentPercent,\r\n state.currentDSCRPercent,\r\n state.currentSellerFiPercent,\r\n state.currentInterestRateType\r\n );\r\n updateTooltipContent(metric, tooltipContent);\r\n }, 50);\r\n }\r\n }\r\n }, 100);\r\n });\r\n\r\n downLabelElement.addEventListener(\"click\", function(e) {\r\n e.preventDefault();\r\n e.stopPropagation();\r\n\r\n updateState({\r\n currentDownPaymentPercent: FINANCIAL_CONSTANTS.SELLER_FI_DOWN_PAYMENT * 100,\r\n currentDSCRPercent: FINANCIAL_CONSTANTS.DEFAULT_DSCR_PERCENTAGE * 100,\r\n currentSellerFiPercent: FINANCIAL_CONSTANTS.SELLER_FI_CARRY * 100\r\n });\r\n\r\n callbacks.updatePercentageLabels();\r\n callbacks.recalculateFinancials();\r\n\r\n setTimeout(() => {\r\n const priceElement = document.getElementById(\"prop-price\");\r\n const noiElement = document.getElementById(\"prop-noi\");\r\n\r\n if (priceElement && noiElement && metric) {\r\n const priceMatch = priceElement.textContent.match(/[\\d,]+/);\r\n const noiMatch = noiElement.textContent.match(/[\\d,.]+/);\r\n\r\n if (priceMatch && noiMatch) {\r\n const price = parseFloat(priceMatch[0].replace(/,/g, \"\"));\r\n let noi = parseFloat(noiMatch[0].replace(/,/g, \"\"));\r\n\r\n if (noiElement.textContent.includes(\"K\")) noi *= 1000;\r\n if (noiElement.textContent.includes(\"M\")) noi *= 1000000;\r\n\r\n removeTooltip(metric);\r\n setTimeout(() => {\r\n const tooltipContent = generateDownPaymentTooltipHTML(\r\n price,\r\n noi,\r\n state.currentDownPaymentPercent,\r\n state.currentDSCRPercent,\r\n state.currentSellerFiPercent,\r\n state.currentInterestRateType\r\n );\r\n updateTooltipContent(metric, tooltipContent);\r\n }, 50);\r\n }\r\n }\r\n }, 100);\r\n });\r\n\r\n if (metric && downLabelElement) {\r\n downLabelElement.classList.add('has-tooltip');\r\n }\r\n\r\n downElement.style.cursor = \"pointer\";\r\n downLabelElement.style.cursor = \"pointer\";\r\n}\r\n"],"names":["updateDiscountButtonText","state","btn","document","getElementById","textContent","currentPriceDiscount","setupDiscountButtonHandler","buttonElement","callbacks","dataset","handlerAttached","updateState","addEventListener","e","preventDefault","stopPropagation","priceElement","getCurrentPrice","updatePriceLabel","recalculateFinancials","setupPriceClickHandler","priceLabelElement","metric","closest","openPriceInput","querySelector","input","createElement","type","value","placeholder","className","style","width","appendChild","focus","done","finish","save","remove","raw","match","String","parseFloat","replace","NaN","Number","isFinite","formatted","Math","round","toLocaleString","baseNOI","originalPrice","priceWasDefaulted","commitPrice","ev","key","test","newDiscount","floor","newPrice","tooltipContent","generatePriceTooltipHTML","updateTooltipContent","resetPrice","attachTooltip","classList","add","cursor","setupCapRateClickHandler","capElement","capLabelElement","select","capManuallySet","currentEstimatedCapRate","isUsingEstimatedCapRate","generateCapRateTooltipHTML","commit","reportedMatch","originalCapRate","includes","originalEstimatedCapRate","FINANCIAL_CONSTANTS","DEFAULT_CAP_RATE","setupNoiClickHandler","noiElement","noiLabelElement","currentPropertyType","current","cachedStrValue","setupAwningLinkHandler","linkElement","address","trim","navigator","clipboard","writeText","catch","window","open","setupDownPaymentClickHandler","downElement","downLabelElement","newDownPercent","currentDownPaymentPercent","newDSCRPercent","currentDSCRPercent","newSellerFiPercent","currentSellerFiPercent","updatePercentageLabels","setTimeout","priceMatch","noiMatch","price","noi","removeTooltip","generateDownPaymentTooltipHTML","currentInterestRateType","SELLER_FI_DOWN_PAYMENT","DEFAULT_DSCR_PERCENTAGE","SELLER_FI_CARRY"],"mappings":"4TAOA,SAASA,yBAAyBC,GAChC,MAAMC,EAAMC,SAASC,eAAe,mBAC/BF,IACLA,EAAIG,YAAcJ,EAAMK,qBAAuB,EAAI,kBAAoB,gBACzE,CAEO,SAASC,2BAA2BC,EAAeC,GACxD,IAAKD,EAAe,OAEpB,GAA8C,SAA1CA,EAAcE,QAAQC,gBAA4B,OACtDH,EAAcE,QAAQC,gBAAkB,OAExC,MAAMV,MAAEA,EAAKW,YAAEA,GAAgBH,EAE/BD,EAAcK,iBAAiB,QAAS,SAASC,GAC/CA,EAAEC,iBACFD,EAAEE,kBAEEf,EAAMK,qBAAuB,EAC/BM,EAAY,CAAEN,qBAAsB,IAEpCM,EAAY,CAAEN,qBAAsB,KAGtC,MAAMW,EAAed,SAASC,eAAe,cACzCa,IACFA,EAAaZ,YAAcI,EAAUS,mBAEvCT,EAAUU,mBACVV,EAAUW,wBACVpB,yBAAyBC,EAC3B,EACF,CAEO,SAASoB,uBAAuBJ,EAAcK,EAAmBb,GACtE,IAAKQ,IAAiBK,EAAmB,OAGzC,GAA6C,SAAzCL,EAAaP,QAAQC,gBAA4B,OACrDM,EAAaP,QAAQC,gBAAkB,OAEvC,MAAMV,MAAEA,EAAKW,YAAEA,GAAgBH,EACzBc,EAASN,EAAaO,QAAQ,WAmBpC,SAASC,iBACP,GAAIR,EAAaS,cAAc,SAAU,OACzC,MAAMC,EAAQxB,SAASyB,cAAc,SACrCD,EAAME,KAAO,OACbF,EAAMG,MAAQ,GACdH,EAAMI,YAAc,UACpBJ,EAAMK,UAAY,cAClBL,EAAMM,MAAMC,MAAQ,QACpBjB,EAAaZ,YAAc,GAC3BY,EAAakB,YAAYR,GACzBA,EAAMS,QAEN,IAAIC,GAAO,EACX,MAAMC,OAAUC,IACd,GAAIF,EAAM,OACVA,GAAO,EACP,MAAMP,EAAQH,EAAMG,MACpBH,EAAMa,SACFD,EA/BR,SAAqBE,GACnB,MAAMC,EAAQC,OAAOF,GAAKC,MAAM,WAC1BZ,EAAQY,EAAQE,WAAWF,EAAM,GAAGG,QAAQ,KAAM,KAAOC,IAC/D,GAAIC,OAAOC,SAASlB,IAAUA,EAAQ,EAAG,CACvC,MAAMmB,EAAY,IAAIC,KAAKC,MAAMrB,GAAOsB,mBACxCxC,EAAY,CAAEyC,QAAS,KAAM/C,qBAAsB,EAAGgD,cAAeL,EAAWM,mBAAmB,IACnGtC,EAAaZ,YAAc4C,CAC7B,CACAxC,EAAUU,mBACVV,EAAUW,wBACVpB,yBAAyBC,EAC3B,CAoBcuD,CAAY1B,GACjBrB,EAAUW,yBAEjBO,EAAMd,iBAAiB,UAAY4C,IAClB,UAAXA,EAAGC,KAAmBD,EAAG1C,iBAAkBuB,QAAO,IAClC,WAAXmB,EAAGC,MAAoBD,EAAG1C,iBAAkBuB,QAAO,MAE9DX,EAAMd,iBAAiB,OAAQ,IAAMyB,QAAO,GAC9C,CAmDA,GAjDArB,EAAaJ,iBAAiB,QAAS,SAASC,GAI9C,GAHAA,EAAEC,iBACFD,EAAEE,kBAEEC,EAAaS,cAAc,SAAU,OAGzC,GADqBzB,EAAMsD,oBAAsB,KAAKI,KAAK1C,EAAaZ,aAAe,IAGrF,YADAoB,iBAIF,IAAImC,EAA4D,GAA9CV,KAAKW,MAAM5D,EAAMK,qBAAuB,IAAW,GACjEsD,EAAc,KAChBA,EAAc,GAGhBhD,EAAY,CAAEN,qBAAsBsD,IAEpC,MAAME,EAAWrD,EAAUS,kBAM3B,GALAD,EAAaZ,YAAcyD,EAC3BrD,EAAUU,mBACVV,EAAUW,wBACVpB,yBAAyBC,GAErBsB,EAAQ,CACV,MAAMwC,EAAiBC,EAAyB/D,EAAMK,sBACtD2D,EAAqB1C,EAAQwC,EAC/B,CACF,GAEAzC,EAAkBT,iBAAiB,QAAS,SAASC,GACnDA,EAAEC,iBACFD,EAAEE,kBAEFJ,EAAY,CAAEN,qBAAsB,IAEpC,MAAM4D,EAAajE,EAAMqD,cAMzB,GALArC,EAAaZ,YAAc6D,EAC3BzD,EAAUU,mBACVV,EAAUW,wBACVpB,yBAAyBC,GAErBsB,EAAQ,CACV,MAAMwC,EAAiBC,EAAyB/D,EAAMK,sBACtD2D,EAAqB1C,EAAQwC,EAC/B,CACF,GAEIxC,EAAQ,CACV,MAAMwC,EAAiBC,EAAyB/D,EAAMK,sBACtD6D,EAAc5C,EAAQwC,GACtBzC,EAAkB8C,UAAUC,IAAI,cAClC,CAEApD,EAAagB,MAAMqC,OAAS,UAC5BhD,EAAkBW,MAAMqC,OAAS,SACnC,CASO,SAASC,yBAAyBC,EAAYC,EAAiBhE,GACpE,IAAK+D,IAAeC,EAAiB,OAGrC,GAA2C,SAAvCD,EAAW9D,QAAQC,gBAA4B,OACnD6D,EAAW9D,QAAQC,gBAAkB,OAErC,MAAMV,MAAEA,EAAKW,YAAEA,GAAgBH,EACzBc,EAASiD,EAAWhD,QAAQ,WAwFlC,GApEAgD,EAAW3D,iBAAiB,QAAS,SAASC,GAI5C,GAHAA,EAAEC,iBACFD,EAAEE,kBAEEwD,EAAW9C,cAAc,SAAU,OAEvC,MAAMgB,GAAS8B,EAAWnE,aAAe,IAAIqC,MAAM,UAC7Cf,EAAQxB,SAASyB,cAAc,SACrCD,EAAME,KAAO,OACbF,EAAMG,MAAQY,EAAQA,EAAM,GAAK,GACjCf,EAAMI,YAAc,QACpBJ,EAAMK,UAAY,YAClBL,EAAMM,MAAMC,MAAQ,OACpBsC,EAAWnE,YAAc,GACzBmE,EAAWrC,YAAYR,GACvBA,EAAMS,QACNT,EAAM+C,SAEN,IAAIrC,GAAO,EACX,MAAMC,OAAUC,IACd,GAAIF,EAAM,OACVA,GAAO,EACP,MAAMP,EAAQH,EAAMG,MAGpBH,EAAMa,SACFD,EA5CR,SAAgBE,GACd,MAAMC,EAAQC,OAAOF,GAAKC,MAAM,UAC1BZ,EAAQY,EAAQE,WAAWF,EAAM,IAAMI,IAU7C,GATIC,OAAOC,SAASlB,IAAUA,EAAQ,GACpClB,EAAY,CACVyC,QAAS,KACTsB,gBAAgB,EAChBC,wBAAyB9C,EACzB+C,yBAAyB,IAG7BpE,EAAUW,wBACNG,EAAQ,CACV,MAAMwC,EAAiBe,EAA2B7E,EAAM4E,yBACpDd,GAAgBE,EAAqB1C,EAAQwC,EACnD,CACF,CA4BcgB,CAAOjD,GACZrB,EAAUW,yBAEjBO,EAAMd,iBAAiB,UAAY4C,IAClB,UAAXA,EAAGC,KAAmBD,EAAG1C,iBAAkBuB,QAAO,IAClC,WAAXmB,EAAGC,MAAoBD,EAAG1C,iBAAkBuB,QAAO,MAE9DX,EAAMd,iBAAiB,OAAQ,IAAMyB,QAAO,GAC9C,GAEAmC,EAAgB5D,iBAAiB,QAAS,SAASC,GACjDA,EAAEC,iBACFD,EAAEE,kBAEF,MAAMgE,EAAgB/E,EAAMgF,kBAAoBhF,EAAMgF,gBAAgBC,SAAS,KAC3EjF,EAAMgF,gBAAgBvC,MAAM,UAC5B,KAEJ,GAAIsC,EAGFR,EAAWnE,YAAc,GAAGuC,WAAWoC,EAAc,OACrDpE,EAAY,CAAEyC,QAAS,KAAMsB,gBAAgB,EAAOE,yBAAyB,QACxE,CACL,MAAMI,EAAkBhF,EAAMkF,0BAAmE,IAAvCC,EAAoBC,iBAC9EzE,EAAY,CACVyC,QAAS,KACTsB,gBAAgB,EAChBC,wBAAyBK,EACzBJ,yBAAyB,GAE7B,CAGA,GAFApE,EAAUW,wBAENG,EAAQ,CACV,MAAMwC,EAAiBe,EAA2B7E,EAAM4E,yBACpDd,GACFE,EAAqB1C,EAAQwC,EAEjC,CACF,GAEIxC,EAAQ,CACV,MAAMwC,EAAiBe,EAA2B7E,EAAM4E,yBACpDd,IACFI,EAAc5C,EAAQwC,GACtBU,EAAgBL,UAAUC,IAAI,eAElC,CAEAG,EAAWvC,MAAMqC,OAAS,UAC1BG,EAAgBxC,MAAMqC,OAAS,SACjC,CAQO,SAASgB,qBAAqBC,EAAYC,EAAiB/E,GAChE,IAAK8E,IAAeC,EAAiB,OAErC,GAA2C,SAAvCD,EAAW7E,QAAQC,gBAA4B,OACnD4E,EAAW7E,QAAQC,gBAAkB,OAErC,MAAMV,MAAEA,EAAKW,YAAEA,GAAgBH,EAW/B8E,EAAW1E,iBAAiB,QAAS,SAASC,GAI5C,GAHAA,EAAEC,iBACFD,EAAEE,kBAEgC,QAA9Bf,EAAMwF,oBAA+B,OACzC,GAAIF,EAAW7D,cAAc,SAAU,OAEvC,MAAMgE,EAAUzF,EAAM0F,gBAAkB5C,OAAOC,SAAS/C,EAAM0F,eAAe7D,OACzEa,OAAO1C,EAAM0F,eAAe7D,OAC5B,GACEH,EAAQxB,SAASyB,cAAc,SACrCD,EAAME,KAAO,OACbF,EAAMG,MAAQ4D,EACd/D,EAAMI,YAAc,oBACpBJ,EAAMK,UAAY,YAClBL,EAAMM,MAAMC,MAAQ,OACpBqD,EAAWlF,YAAc,GACzBkF,EAAWpD,YAAYR,GACvBA,EAAMS,QACNT,EAAM+C,SAEN,IAAIrC,GAAO,EACX,MAAMC,OAAUC,IACVF,IACJA,GAAO,EACHE,EAlCR,SAAgBE,GACd,MAAMC,EAAQC,OAAOF,GAAKC,MAAM,WAC1BZ,EAAQY,EAAQE,WAAWF,EAAM,GAAGG,QAAQ,KAAM,KAAOC,IAC3DC,OAAOC,SAASlB,IAAUA,EAAQ,GACpClB,EAAY,CAAE+E,eAAgB,CAAE7D,QAAOD,KAAM,SAAWwB,QAAS,KAAMsB,gBAAgB,IAEzFlE,EAAUW,uBACZ,CA2Bc2D,CAAOpD,EAAMG,OAClBrB,EAAUW,0BAEjBO,EAAMd,iBAAiB,UAAY4C,IAClB,UAAXA,EAAGC,KAAmBD,EAAG1C,iBAAkBuB,QAAO,IAClC,WAAXmB,EAAGC,MAAoBD,EAAG1C,iBAAkBuB,QAAO,MAE9DX,EAAMd,iBAAiB,OAAQ,IAAMyB,QAAO,GAC9C,GAEAkD,EAAgB3E,iBAAiB,QAAS,SAASC,GACjDA,EAAEC,iBACFD,EAAEE,kBAEgC,QAA9Bf,EAAMwF,sBACV7E,EAAY,CAAE+E,eAAgB,KAAMtC,QAAS,OAC7C5C,EAAUW,wBACZ,GAEAmE,EAAWtD,MAAMqC,OAAS,UAC1BkB,EAAgBvD,MAAMqC,OAAS,SACjC,CAMO,SAASsB,uBAAuBC,GAChCA,GAEuC,SAAxCA,EAAYnF,QAAQC,kBACxBkF,EAAYnF,QAAQC,gBAAkB,OAEtCkF,EAAYhF,iBAAiB,QAAS,SAASC,GAC7CA,EAAEC,iBACFD,EAAEE,kBAEF,MAAM8E,EAAU3F,SAASC,eAAe,cAAcC,aAAa0F,QAAU,GACzED,GAAWE,UAAUC,WAAWC,WAClCF,UAAUC,UAAUC,UAAUJ,GAASK,MAAM,QAE/CC,OAAOC,KAAK,uCAAwC,SAAU,WAChE,GACF,CAEO,SAASC,6BAA6BC,EAAaC,EAAkB/F,GAC1E,IAAK8F,IAAgBC,EAAkB,OAGvC,GAA4C,SAAxCD,EAAY7F,QAAQC,gBAA4B,OACpD4F,EAAY7F,QAAQC,gBAAkB,OAEtC,MAAMV,MAAEA,EAAKW,YAAEA,GAAgBH,EACzBc,EAASgF,EAAY/E,QAAQ,WAEnC+E,EAAY1F,iBAAiB,QAAS,SAASC,GAC7CA,EAAEC,iBACFD,EAAEE,kBAEF,IAAIyF,EAAiBxG,EAAMyG,0BAA4B,GACnDC,EAAiB1G,EAAM2G,mBAAqB,GAC5CC,EAAqB5G,EAAM6G,uBAAyB,GAEpDL,EAAiB,IACnBA,EAAiB,GACjBE,EAAiB,GACjBE,EAAqB,IAGvBjG,EAAY,CACV8F,0BAA2BD,EAC3BG,mBAAoBD,EACpBG,uBAAwBD,IAG1BpG,EAAUsG,yBACVtG,EAAUW,wBAEV4F,WAAW,KACT,MAAM/F,EAAed,SAASC,eAAe,cACvCmF,EAAapF,SAASC,eAAe,YAE3C,GAAIa,GAAgBsE,GAAchE,EAAQ,CACxC,MAAM0F,EAAahG,EAAaZ,YAAYqC,MAAM,UAC5CwE,EAAW3B,EAAWlF,YAAYqC,MAAM,WAE9C,GAAIuE,GAAcC,EAAU,CAC1B,MAAMC,EAAQvE,WAAWqE,EAAW,GAAGpE,QAAQ,KAAM,KACrD,IAAIuE,EAAMxE,WAAWsE,EAAS,GAAGrE,QAAQ,KAAM,KAE3C0C,EAAWlF,YAAY6E,SAAS,OAAMkC,GAAO,KAC7C7B,EAAWlF,YAAY6E,SAAS,OAAMkC,GAAO,KAEjDC,EAAc9F,GACdyF,WAAW,KACT,MAAMjD,EAAiBuD,EACrBH,EACAC,EACAnH,EAAMyG,0BACNzG,EAAM2G,mBACN3G,EAAM6G,uBACN7G,EAAMsH,yBAERtD,EAAqB1C,EAAQwC,IAC5B,GACL,CACF,GACC,IACL,GAEAyC,EAAiB3F,iBAAiB,QAAS,SAASC,GAClDA,EAAEC,iBACFD,EAAEE,kBAEFJ,EAAY,CACV8F,0BAAwE,IAA7CtB,EAAoBoC,uBAC/CZ,mBAAkE,IAA9CxB,EAAoBqC,wBACxCX,uBAA8D,IAAtC1B,EAAoBsC,kBAG9CjH,EAAUsG,yBACVtG,EAAUW,wBAEV4F,WAAW,KACT,MAAM/F,EAAed,SAASC,eAAe,cACvCmF,EAAapF,SAASC,eAAe,YAE3C,GAAIa,GAAgBsE,GAAchE,EAAQ,CACxC,MAAM0F,EAAahG,EAAaZ,YAAYqC,MAAM,UAC5CwE,EAAW3B,EAAWlF,YAAYqC,MAAM,WAE9C,GAAIuE,GAAcC,EAAU,CAC1B,MAAMC,EAAQvE,WAAWqE,EAAW,GAAGpE,QAAQ,KAAM,KACrD,IAAIuE,EAAMxE,WAAWsE,EAAS,GAAGrE,QAAQ,KAAM,KAE3C0C,EAAWlF,YAAY6E,SAAS,OAAMkC,GAAO,KAC7C7B,EAAWlF,YAAY6E,SAAS,OAAMkC,GAAO,KAEjDC,EAAc9F,GACdyF,WAAW,KACT,MAAMjD,EAAiBuD,EACrBH,EACAC,EACAnH,EAAMyG,0BACNzG,EAAM2G,mBACN3G,EAAM6G,uBACN7G,EAAMsH,yBAERtD,EAAqB1C,EAAQwC,IAC5B,GACL,CACF,GACC,IACL,GAEIxC,GAAUiF,GACZA,EAAiBpC,UAAUC,IAAI,eAGjCkC,EAAYtE,MAAMqC,OAAS,UAC3BkC,EAAiBvE,MAAMqC,OAAS,SAClC"}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{computeManualOverrideNOI as t,resolveCapRateProvenance as a}from"../financial/capRate.js";import{calculateFinancials as e}from"../financial/calculateFinancials.js";import{syncInterestRateForUnits as i}from"./interestRateSync.js";import{FINANCIAL_CONSTANTS as n}from"../../config/financial.js";import{normalizeWhitespace as r}from"../../formatting/text.js";const p=["name","price","capRate","contact","phone","listingDate"];function isValidListingShape(t){return!(!t||"object"!=typeof t)&&p.every(a=>"string"==typeof t[a])}function createFinance({ctx:c,adapter:l,render:o}){const{state:s,updateState:u}=c;function applyCapRate(t){const{isDefault:e,estimated:i,num:r,displayCap:p}=a(t.capRate,100*n.DEFAULT_CAP_RATE);if(t.capRate=p,e)return s.originalCapRate||u({originalCapRate:p}),s.originalMultifamilyCapRate||u({originalMultifamilyCapRate:`${r}%`}),void u({currentEstimatedCapRate:r,isUsingEstimatedCapRate:!0,originalEstimatedCapRate:r});u({isUsingEstimatedCapRate:i}),i&&null!==r&&u({currentEstimatedCapRate:r}),s.originalCapRate||u({originalCapRate:p}),s.originalMultifamilyCapRate||null===r||u({originalMultifamilyCapRate:`${r}%`})}return{applyCapRate:applyCapRate,handlePropertyTypeChange:function(){const t=document.getElementById("ln-property-type");if(!t)return;const a=t.value;return u({currentPropertyType:a}),"str"!==a&&u({cachedSTRData:null}),u({baseNOI:null}),i(s,u),a},recalculateFinancials:async function(){const a=document.getElementById("prop-price");if(document.getElementById("prop-name"),!a)return;const i=o.getCurrentPrice()||a.textContent;let n;if(s.isUsingEstimatedCapRate)n=`${s.currentEstimatedCapRate}%*`;else{const t=document.getElementById("prop-cap");n=t?t.textContent:"8%"}if("str"===s.currentPropertyType&&u({cachedSTRData:null}),s.capManuallySet){const a=t(s.originalPrice||i,s.currentEstimatedCapRate);null!=a&&u({baseNOI:a})}const r=await e(c,i,n,s.currentPropertyType);o.applyFinancials(r),o.updateActiveCapDisplay(),o.updateEquityDisplay()},scrapeAndApply:function(){const t=l.scrape();if(!isValidListingShape(t))return null;for(const a of p)t[a]=r(t[a]);const a=t.priceWasDefaulted??"Not found"===t.price;return u({priceWasDefaulted:a}),a||s.originalPrice||u({originalPrice:t.originalPrice??t.price}),applyCapRate(t),t}}}export{createFinance,isValidListingShape};
|
|
1
|
+
import{computeManualOverrideNOI as t,resolveCapRateProvenance as a}from"../financial/capRate.js";import{calculateFinancials as e}from"../financial/calculateFinancials.js";import{syncInterestRateForUnits as i}from"./interestRateSync.js";import{FINANCIAL_CONSTANTS as n}from"../../config/financial.js";import{normalizeWhitespace as r}from"../../formatting/text.js";const p=["name","price","capRate","contact","phone","listingDate"];function isValidListingShape(t){return!(!t||"object"!=typeof t)&&p.every(a=>"string"==typeof t[a])}function createFinance({ctx:c,adapter:l,render:o}){const{state:s,updateState:u}=c;function applyCapRate(t){const{isDefault:e,estimated:i,num:r,displayCap:p}=a(t.capRate,100*n.DEFAULT_CAP_RATE);if(t.capRate=p,e)return s.originalCapRate||u({originalCapRate:p}),s.originalMultifamilyCapRate||u({originalMultifamilyCapRate:`${r}%`}),void u({currentEstimatedCapRate:r,isUsingEstimatedCapRate:!0,originalEstimatedCapRate:r});u({isUsingEstimatedCapRate:i}),i&&null!==r&&u({currentEstimatedCapRate:r}),s.originalCapRate||u({originalCapRate:p}),s.originalMultifamilyCapRate||null===r||u({originalMultifamilyCapRate:`${r}%`})}return{applyCapRate:applyCapRate,handlePropertyTypeChange:function(){const t=document.getElementById("ln-property-type");if(!t)return;const a=t.value;return u({currentPropertyType:a}),"str"!==a&&u({cachedSTRData:null}),u({baseNOI:null}),i(s,u),a},recalculateFinancials:async function(){const a=document.getElementById("prop-price");if(document.getElementById("prop-name"),!a)return;const i=o.getCurrentPrice()||a.textContent;let n;if(s.isUsingEstimatedCapRate)n=`${s.currentEstimatedCapRate}%*`;else{const t=document.getElementById("prop-cap");n=s.originalCapRate||(t?t.textContent:"8%")}if("str"===s.currentPropertyType&&u({cachedSTRData:null}),s.capManuallySet){const a=t(s.originalPrice||i,s.currentEstimatedCapRate);null!=a&&u({baseNOI:a})}const r=await e(c,i,n,s.currentPropertyType);o.applyFinancials(r),o.updateActiveCapDisplay(),o.updateEquityDisplay()},scrapeAndApply:function(){const t=l.scrape();if(!isValidListingShape(t))return null;for(const a of p)t[a]=r(t[a]);const a=t.priceWasDefaulted??"Not found"===t.price;return u({priceWasDefaulted:a}),a||s.originalPrice||u({originalPrice:t.originalPrice??t.price}),applyCapRate(t),t}}}export{createFinance,isValidListingShape};
|
|
2
2
|
//# sourceMappingURL=finance.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"finance.js","sources":["../../../src/browser/widget/finance.js"],"sourcesContent":["// Finance unit (orchestration): applies cap-rate provenance to ctx, applies the scrape-derived\r\n// state, and recomputes the financial metrics. The pure rules live in financial/capRate.js; this\r\n// module is the thin state/DOM glue around them. Extracted verbatim from createAnalyzer (T12).\r\n\r\nimport { computeManualOverrideNOI, resolveCapRateProvenance } from \"../financial/capRate.js\";\r\nimport { calculateFinancials } from \"../financial/calculateFinancials.js\";\r\nimport { syncInterestRateForUnits } from \"./interestRateSync.js\";\r\nimport { FINANCIAL_CONSTANTS } from \"../../config/financial.js\";\r\nimport { normalizeWhitespace } from \"../../formatting/text.js\";\r\n\r\n// The Listing fields every scraper must return as strings (default \"Not found\"). A missing or\r\n// non-string field means the scraper broke; the engine refuses to render/export rather than\r\n// letting `undefined` flow into the NOI/COCR math and silently paint wrong numbers (fail-loud).\r\nconst LISTING_CONTRACT_FIELDS = [\"name\", \"price\", \"capRate\", \"contact\", \"phone\", \"listingDate\"];\r\n\r\nexport function isValidListingShape(data) {\r\n if (!data || typeof data !== \"object\") return false;\r\n return LISTING_CONTRACT_FIELDS.every((field) => typeof data[field] === \"string\");\r\n}\r\n\r\nexport function createFinance({ ctx, adapter, render }) {\r\n const { state, updateState } = ctx;\r\n\r\n // Resolve the cap-rate provenance from the scraped string and write the cap state the\r\n // financial calc + discount handlers read. The default (DEFAULT_CAP_RATE * 100 = 5,\r\n // whole-number percent) fixes the latent no-cap bug where the decimal 0.05 was stored\r\n // and then divided by 100, computing NOI at 0.05%.\r\n function applyCapRate(listing) {\r\n const { isDefault, estimated, num, displayCap } = resolveCapRateProvenance(\r\n listing.capRate,\r\n FINANCIAL_CONSTANTS.DEFAULT_CAP_RATE * 100\r\n );\r\n listing.capRate = displayCap;\r\n\r\n if (isDefault) {\r\n if (!state.originalCapRate) updateState({ originalCapRate: displayCap });\r\n if (!state.originalMultifamilyCapRate) updateState({ originalMultifamilyCapRate: `${num}%` });\r\n updateState({\r\n currentEstimatedCapRate: num,\r\n isUsingEstimatedCapRate: true,\r\n originalEstimatedCapRate: num,\r\n });\r\n return;\r\n }\r\n\r\n updateState({ isUsingEstimatedCapRate: estimated });\r\n if (estimated && num !== null) updateState({ currentEstimatedCapRate: num });\r\n if (!state.originalCapRate) updateState({ originalCapRate: displayCap });\r\n if (!state.originalMultifamilyCapRate && num !== null) {\r\n updateState({ originalMultifamilyCapRate: `${num}%` });\r\n }\r\n }\r\n\r\n // Scrape the page and apply the universal scrape-derived state (was extractData's side\r\n // effects). Returns the resolved listing (capRate normalized to a display string) or null\r\n // when the scrape is malformed.\r\n function scrapeAndApply() {\r\n const listing = adapter.scrape();\r\n if (!isValidListingShape(listing)) return null;\r\n\r\n // Normalize whitespace on every contract string field centrally, so adapters stay pure\r\n // scrapers and no consumer (panel or export) ever sees the interior newlines/tabs that\r\n // site markup splits text across (e.g. a broker name on two lines). \"Not found\" is\r\n // unchanged. This is the single enforcement point for the data contract's \"normalize text\".\r\n for (const field of LISTING_CONTRACT_FIELDS) {\r\n listing[field] = normalizeWhitespace(listing[field]);\r\n }\r\n\r\n const priceWasDefaulted = listing.priceWasDefaulted ?? (listing.price === \"Not found\");\r\n updateState({ priceWasDefaulted });\r\n\r\n if (!priceWasDefaulted && !state.originalPrice) {\r\n updateState({ originalPrice: listing.originalPrice ?? listing.price });\r\n }\r\n\r\n applyCapRate(listing);\r\n return listing;\r\n }\r\n\r\n function handlePropertyTypeChange() {\r\n const dropdown = document.getElementById(\"ln-property-type\");\r\n if (!dropdown) return;\r\n const newType = dropdown.value;\r\n updateState({ currentPropertyType: newType });\r\n if (newType !== \"str\") updateState({ cachedSTRData: null });\r\n updateState({ baseNOI: null });\r\n syncInterestRateForUnits(state, updateState);\r\n return newType;\r\n }\r\n\r\n async function recalculateFinancials() {\r\n const priceElement = document.getElementById(\"prop-price\");\r\n const addressElement = document.getElementById(\"prop-name\");\r\n if (!priceElement) return;\r\n\r\n const priceText = render.getCurrentPrice() || priceElement.textContent;\r\n const address = addressElement?.textContent || \"\";\r\n let capRateText;\r\n if (state.isUsingEstimatedCapRate) {\r\n capRateText = `${state.currentEstimatedCapRate}%*`;\r\n } else {\r\n const capElement = document.getElementById(\"prop-cap\");\r\n capRateText = capElement ? capElement.textContent : \"8%\";\r\n }\r\n\r\n if (state.currentPropertyType === \"str\") updateState({ cachedSTRData: null });\r\n\r\n // Manual cap override: clicking the cap rate sets NOI = original price x cap for EVERY type\r\n // (analyst intent), so the active cap moves with the click even for STR/assisted whose NOI\r\n // is otherwise the type estimate / bedroom value. Pre-seed baseNOI so calculateFinancials\r\n // uses it instead of recomputing from the type model.\r\n if (state.capManuallySet) {\r\n const noi = computeManualOverrideNOI(state.originalPrice || priceText, state.currentEstimatedCapRate);\r\n if (noi != null) updateState({ baseNOI: noi });\r\n }\r\n\r\n const financials = await calculateFinancials(ctx, priceText, capRateText, state.currentPropertyType, address);\r\n render.applyFinancials(financials);\r\n render.updateActiveCapDisplay();\r\n render.updateEquityDisplay();\r\n }\r\n\r\n return { applyCapRate, handlePropertyTypeChange, recalculateFinancials, scrapeAndApply };\r\n}\r\n"],"names":["LISTING_CONTRACT_FIELDS","isValidListingShape","data","every","field","createFinance","ctx","adapter","render","state","updateState","applyCapRate","listing","isDefault","estimated","num","displayCap","resolveCapRateProvenance","capRate","FINANCIAL_CONSTANTS","DEFAULT_CAP_RATE","originalCapRate","originalMultifamilyCapRate","currentEstimatedCapRate","isUsingEstimatedCapRate","originalEstimatedCapRate","handlePropertyTypeChange","dropdown","document","getElementById","newType","value","currentPropertyType","cachedSTRData","baseNOI","syncInterestRateForUnits","recalculateFinancials","async","priceElement","priceText","getCurrentPrice","textContent","capRateText","capElement","capManuallySet","noi","computeManualOverrideNOI","originalPrice","financials","calculateFinancials","applyFinancials","updateActiveCapDisplay","updateEquityDisplay","scrapeAndApply","scrape","normalizeWhitespace","priceWasDefaulted","price"],"mappings":"2WAaA,MAAMA,EAA0B,CAAC,OAAQ,QAAS,UAAW,UAAW,QAAS,eAE1E,SAASC,oBAAoBC,GAClC,SAAKA,GAAwB,iBAATA,IACbF,EAAwBG,MAAOC,GAAiC,iBAAhBF,EAAKE,GAC9D,CAEO,SAASC,eAAcC,IAAEA,EAAGC,QAAEA,EAAOC,OAAEA,IAC5C,MAAMC,MAAEA,EAAKC,YAAEA,GAAgBJ,EAM/B,SAASK,aAAaC,GACpB,MAAMC,UAAEA,EAASC,UAAEA,EAASC,IAAEA,EAAGC,WAAEA,GAAeC,EAChDL,EAAQM,QAC+B,IAAvCC,EAAoBC,kBAItB,GAFAR,EAAQM,QAAUF,EAEdH,EAQF,OAPKJ,EAAMY,iBAAiBX,EAAY,CAAEW,gBAAiBL,IACtDP,EAAMa,4BAA4BZ,EAAY,CAAEY,2BAA4B,GAAGP,YACpFL,EAAY,CACVa,wBAAyBR,EACzBS,yBAAyB,EACzBC,yBAA0BV,IAK9BL,EAAY,CAAEc,wBAAyBV,IACnCA,GAAqB,OAARC,GAAcL,EAAY,CAAEa,wBAAyBR,IACjEN,EAAMY,iBAAiBX,EAAY,CAAEW,gBAAiBL,IACtDP,EAAMa,4BAAsC,OAARP,GACvCL,EAAY,CAAEY,2BAA4B,GAAGP,MAEjD,
|
|
1
|
+
{"version":3,"file":"finance.js","sources":["../../../src/browser/widget/finance.js"],"sourcesContent":["// Finance unit (orchestration): applies cap-rate provenance to ctx, applies the scrape-derived\r\n// state, and recomputes the financial metrics. The pure rules live in financial/capRate.js; this\r\n// module is the thin state/DOM glue around them. Extracted verbatim from createAnalyzer (T12).\r\n\r\nimport { computeManualOverrideNOI, resolveCapRateProvenance } from \"../financial/capRate.js\";\r\nimport { calculateFinancials } from \"../financial/calculateFinancials.js\";\r\nimport { syncInterestRateForUnits } from \"./interestRateSync.js\";\r\nimport { FINANCIAL_CONSTANTS } from \"../../config/financial.js\";\r\nimport { normalizeWhitespace } from \"../../formatting/text.js\";\r\n\r\n// The Listing fields every scraper must return as strings (default \"Not found\"). A missing or\r\n// non-string field means the scraper broke; the engine refuses to render/export rather than\r\n// letting `undefined` flow into the NOI/COCR math and silently paint wrong numbers (fail-loud).\r\nconst LISTING_CONTRACT_FIELDS = [\"name\", \"price\", \"capRate\", \"contact\", \"phone\", \"listingDate\"];\r\n\r\nexport function isValidListingShape(data) {\r\n if (!data || typeof data !== \"object\") return false;\r\n return LISTING_CONTRACT_FIELDS.every((field) => typeof data[field] === \"string\");\r\n}\r\n\r\nexport function createFinance({ ctx, adapter, render }) {\r\n const { state, updateState } = ctx;\r\n\r\n // Resolve the cap-rate provenance from the scraped string and write the cap state the\r\n // financial calc + discount handlers read. The default (DEFAULT_CAP_RATE * 100 = 5,\r\n // whole-number percent) fixes the latent no-cap bug where the decimal 0.05 was stored\r\n // and then divided by 100, computing NOI at 0.05%.\r\n function applyCapRate(listing) {\r\n const { isDefault, estimated, num, displayCap } = resolveCapRateProvenance(\r\n listing.capRate,\r\n FINANCIAL_CONSTANTS.DEFAULT_CAP_RATE * 100\r\n );\r\n listing.capRate = displayCap;\r\n\r\n if (isDefault) {\r\n if (!state.originalCapRate) updateState({ originalCapRate: displayCap });\r\n if (!state.originalMultifamilyCapRate) updateState({ originalMultifamilyCapRate: `${num}%` });\r\n updateState({\r\n currentEstimatedCapRate: num,\r\n isUsingEstimatedCapRate: true,\r\n originalEstimatedCapRate: num,\r\n });\r\n return;\r\n }\r\n\r\n updateState({ isUsingEstimatedCapRate: estimated });\r\n if (estimated && num !== null) updateState({ currentEstimatedCapRate: num });\r\n if (!state.originalCapRate) updateState({ originalCapRate: displayCap });\r\n if (!state.originalMultifamilyCapRate && num !== null) {\r\n updateState({ originalMultifamilyCapRate: `${num}%` });\r\n }\r\n }\r\n\r\n // Scrape the page and apply the universal scrape-derived state (was extractData's side\r\n // effects). Returns the resolved listing (capRate normalized to a display string) or null\r\n // when the scrape is malformed.\r\n function scrapeAndApply() {\r\n const listing = adapter.scrape();\r\n if (!isValidListingShape(listing)) return null;\r\n\r\n // Normalize whitespace on every contract string field centrally, so adapters stay pure\r\n // scrapers and no consumer (panel or export) ever sees the interior newlines/tabs that\r\n // site markup splits text across (e.g. a broker name on two lines). \"Not found\" is\r\n // unchanged. This is the single enforcement point for the data contract's \"normalize text\".\r\n for (const field of LISTING_CONTRACT_FIELDS) {\r\n listing[field] = normalizeWhitespace(listing[field]);\r\n }\r\n\r\n const priceWasDefaulted = listing.priceWasDefaulted ?? (listing.price === \"Not found\");\r\n updateState({ priceWasDefaulted });\r\n\r\n if (!priceWasDefaulted && !state.originalPrice) {\r\n updateState({ originalPrice: listing.originalPrice ?? listing.price });\r\n }\r\n\r\n applyCapRate(listing);\r\n return listing;\r\n }\r\n\r\n function handlePropertyTypeChange() {\r\n const dropdown = document.getElementById(\"ln-property-type\");\r\n if (!dropdown) return;\r\n const newType = dropdown.value;\r\n updateState({ currentPropertyType: newType });\r\n if (newType !== \"str\") updateState({ cachedSTRData: null });\r\n updateState({ baseNOI: null });\r\n syncInterestRateForUnits(state, updateState);\r\n return newType;\r\n }\r\n\r\n async function recalculateFinancials() {\r\n const priceElement = document.getElementById(\"prop-price\");\r\n const addressElement = document.getElementById(\"prop-name\");\r\n if (!priceElement) return;\r\n\r\n const priceText = render.getCurrentPrice() || priceElement.textContent;\r\n const address = addressElement?.textContent || \"\";\r\n let capRateText;\r\n if (state.isUsingEstimatedCapRate) {\r\n capRateText = `${state.currentEstimatedCapRate}%*`;\r\n } else {\r\n // Use the scraped reported cap (authoritative), not the painted cap cell — the cell shows\r\n // the active cap (NOI/price), which is \"N/A\" when price is missing and would break the\r\n // recompute when a price is later entered manually.\r\n const capElement = document.getElementById(\"prop-cap\");\r\n capRateText = state.originalCapRate || (capElement ? capElement.textContent : \"8%\");\r\n }\r\n\r\n if (state.currentPropertyType === \"str\") updateState({ cachedSTRData: null });\r\n\r\n // Manual cap override: clicking the cap rate sets NOI = original price x cap for EVERY type\r\n // (analyst intent), so the active cap moves with the click even for STR/assisted whose NOI\r\n // is otherwise the type estimate / bedroom value. Pre-seed baseNOI so calculateFinancials\r\n // uses it instead of recomputing from the type model.\r\n if (state.capManuallySet) {\r\n const noi = computeManualOverrideNOI(state.originalPrice || priceText, state.currentEstimatedCapRate);\r\n if (noi != null) updateState({ baseNOI: noi });\r\n }\r\n\r\n const financials = await calculateFinancials(ctx, priceText, capRateText, state.currentPropertyType, address);\r\n render.applyFinancials(financials);\r\n render.updateActiveCapDisplay();\r\n render.updateEquityDisplay();\r\n }\r\n\r\n return { applyCapRate, handlePropertyTypeChange, recalculateFinancials, scrapeAndApply };\r\n}\r\n"],"names":["LISTING_CONTRACT_FIELDS","isValidListingShape","data","every","field","createFinance","ctx","adapter","render","state","updateState","applyCapRate","listing","isDefault","estimated","num","displayCap","resolveCapRateProvenance","capRate","FINANCIAL_CONSTANTS","DEFAULT_CAP_RATE","originalCapRate","originalMultifamilyCapRate","currentEstimatedCapRate","isUsingEstimatedCapRate","originalEstimatedCapRate","handlePropertyTypeChange","dropdown","document","getElementById","newType","value","currentPropertyType","cachedSTRData","baseNOI","syncInterestRateForUnits","recalculateFinancials","async","priceElement","priceText","getCurrentPrice","textContent","capRateText","capElement","capManuallySet","noi","computeManualOverrideNOI","originalPrice","financials","calculateFinancials","applyFinancials","updateActiveCapDisplay","updateEquityDisplay","scrapeAndApply","scrape","normalizeWhitespace","priceWasDefaulted","price"],"mappings":"2WAaA,MAAMA,EAA0B,CAAC,OAAQ,QAAS,UAAW,UAAW,QAAS,eAE1E,SAASC,oBAAoBC,GAClC,SAAKA,GAAwB,iBAATA,IACbF,EAAwBG,MAAOC,GAAiC,iBAAhBF,EAAKE,GAC9D,CAEO,SAASC,eAAcC,IAAEA,EAAGC,QAAEA,EAAOC,OAAEA,IAC5C,MAAMC,MAAEA,EAAKC,YAAEA,GAAgBJ,EAM/B,SAASK,aAAaC,GACpB,MAAMC,UAAEA,EAASC,UAAEA,EAASC,IAAEA,EAAGC,WAAEA,GAAeC,EAChDL,EAAQM,QAC+B,IAAvCC,EAAoBC,kBAItB,GAFAR,EAAQM,QAAUF,EAEdH,EAQF,OAPKJ,EAAMY,iBAAiBX,EAAY,CAAEW,gBAAiBL,IACtDP,EAAMa,4BAA4BZ,EAAY,CAAEY,2BAA4B,GAAGP,YACpFL,EAAY,CACVa,wBAAyBR,EACzBS,yBAAyB,EACzBC,yBAA0BV,IAK9BL,EAAY,CAAEc,wBAAyBV,IACnCA,GAAqB,OAARC,GAAcL,EAAY,CAAEa,wBAAyBR,IACjEN,EAAMY,iBAAiBX,EAAY,CAAEW,gBAAiBL,IACtDP,EAAMa,4BAAsC,OAARP,GACvCL,EAAY,CAAEY,2BAA4B,GAAGP,MAEjD,CA0EA,MAAO,CAAEJ,0BAAce,yBA9CvB,WACE,MAAMC,EAAWC,SAASC,eAAe,oBACzC,IAAKF,EAAU,OACf,MAAMG,EAAUH,EAASI,MAKzB,OAJArB,EAAY,CAAEsB,oBAAqBF,IACnB,QAAZA,GAAmBpB,EAAY,CAAEuB,cAAe,OACpDvB,EAAY,CAAEwB,QAAS,OACvBC,EAAyB1B,EAAOC,GACzBoB,CACT,EAqCiDM,sBAnCjDC,iBACE,MAAMC,EAAeV,SAASC,eAAe,cAE7C,GADuBD,SAASC,eAAe,cAC1CS,EAAc,OAEnB,MAAMC,EAAY/B,EAAOgC,mBAAqBF,EAAaG,YAE3D,IAAIC,EACJ,GAAIjC,EAAMe,wBACRkB,EAAc,GAAGjC,EAAMc,gCAClB,CAIL,MAAMoB,EAAaf,SAASC,eAAe,YAC3Ca,EAAcjC,EAAMY,kBAAoBsB,EAAaA,EAAWF,YAAc,KAChF,CAQA,GANkC,QAA9BhC,EAAMuB,qBAA+BtB,EAAY,CAAEuB,cAAe,OAMlExB,EAAMmC,eAAgB,CACxB,MAAMC,EAAMC,EAAyBrC,EAAMsC,eAAiBR,EAAW9B,EAAMc,yBAClE,MAAPsB,GAAanC,EAAY,CAAEwB,QAASW,GAC1C,CAEA,MAAMG,QAAmBC,EAAoB3C,EAAKiC,EAAWG,EAAajC,EAAMuB,qBAChFxB,EAAO0C,gBAAgBF,GACvBxC,EAAO2C,yBACP3C,EAAO4C,qBACT,EAEwEC,eArExE,WACE,MAAMzC,EAAUL,EAAQ+C,SACxB,IAAKrD,oBAAoBW,GAAU,OAAO,KAM1C,IAAK,MAAMR,KAASJ,EAClBY,EAAQR,GAASmD,EAAoB3C,EAAQR,IAG/C,MAAMoD,EAAoB5C,EAAQ4C,mBAAwC,cAAlB5C,EAAQ6C,MAQhE,OAPA/C,EAAY,CAAE8C,sBAETA,GAAsB/C,EAAMsC,eAC/BrC,EAAY,CAAEqC,cAAenC,EAAQmC,eAAiBnC,EAAQ6C,QAGhE9C,aAAaC,GACNA,CACT,EAiDF"}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{hasTooltip as e,attachTooltip as t,updateTooltipContent as n}from"../ui/tooltip-manager.js";import{generateDownPaymentTooltipHTML as r,generateCashFlowTooltipHTML as o}from"../financial/tooltip-content-generators.js";import{parseFinancialData as c,parseCashFlowData as s}from"../financial/tooltip-calculations.js";import{parsePriceNumber as i,computeActiveCapDisplay as a,parseReportedCap as l}from"../financial/capRate.js";import{equityPercentFromDebt as p}from"../../financial/calculations.js";import{formatCurrency as u}from"../../financial/formatters.js";import{PROPERTY_TYPE_CONSTANTS as d}from"../../config/property-types.js";const m=["prop-noi","prop-down","prop-net","prop-seller-fi","prop-cocr-30","prop-cocr-15","prop-assignment","prop-dscr","prop-sf","prop-cashflow"];function createRender({ctx:g}){const{state:f,updateState:y}=g;function getCurrentPrice(){if(f.originalPrice&&f.currentPriceDiscount>0){const e=parseFloat(f.originalPrice.replace(/[$,]/g,""))*(1-f.currentPriceDiscount/100);return`$${Math.round(e).toLocaleString()}`}return f.originalPrice}function updatePercentageLabels(){const e=document.querySelector("#prop-down")?.closest(".metric")?.querySelector(".metric-label");e&&(e.textContent=`Down (${f.currentDownPaymentPercent}%)`);const t=document.querySelector("#prop-seller-fi")?.closest(".metric")?.querySelector(".metric-label");t&&(t.textContent=`Seller FI (${f.currentSellerFiPercent}%)`);const n=document.querySelector("#prop-dscr")?.closest(".metric")?.querySelector(".metric-label");n&&(n.textContent=`DSCR (${f.currentDSCRPercent}%)`)}function updateElement(e,t){const n=document.getElementById(e);n&&(n.textContent=t)}return{applyFinancials:function(i){let a=!1;if(i){const l={"prop-noi":i.noi,"prop-down":i.down,"prop-net":i.netToBuyer,"prop-seller-fi":i.sellerFi,"prop-cocr-30":i.cocr30,"prop-cocr-15":i.priceForCOCR15,"prop-assignment":i.assignment,"prop-dscr":i.dscr,"prop-sf":i.sfPayment,"prop-cashflow":i.cashFlow};a=i.rawCashFlow<0;for(const[e,t]of Object.entries(l))updateElement(e,t);setTimeout(()=>{!function(){const o=document.getElementById("prop-down"),s=document.getElementById("prop-price"),i=document.getElementById("prop-noi");if(!o||!s||!i)return;const a=c(s.textContent,i.textContent);if(a){const c=r(a.price,a.noi,f.currentDownPaymentPercent,f.currentDSCRPercent,f.currentSellerFiPercent),s=o.closest(".metric");s&&(e(s)?n(s,c):t(s,c))}}(),function(){const r=document.getElementById("prop-cashflow"),c=document.getElementById("prop-price");if(!r||!c)return;const i=s(c.textContent,r.textContent);if(i){const c=o(i.price,i.monthlyCashFlow),s=r.closest(".metric");if(s){const r=s.querySelector(".metric-label");r&&r.classList.add("has-tooltip"),e(s)?n(s,c):t(s,c)}}}()},100)}else m.forEach(e=>updateElement(e,"N/A"));updatePercentageLabels(),function(){const r="str"===f.currentPropertyType,o=document.getElementById("prop-noi-awning");o&&(o.style.display=r?"inline-block":"none");const c=document.getElementById("prop-noi");if(!c)return;if(c.style.cursor=r?"pointer":"",c.querySelector("input"))return;const s=c.closest(".metric");if(r&&f.cachedStrValue&&Number.isFinite(f.cachedStrValue.value)){c.textContent=`${c.textContent}*`;const r=f.cachedStrValue.value,o=d.STR.NOI_PERCENTAGE,i=`<strong>Manual STR gross (Awning):</strong> ${u(r)}/yr<br>NOI = gross × ${Math.round(100*o)}% = ${u(r*o)}<hr><em>Click NOI to edit; click the label to reset to the estimate.</em>`;s&&(e(s)?n(s,i):(t(s,i),s.querySelector(".metric-label")?.classList.add("has-tooltip")))}else r&&s&&e(s)&&n(s,"<strong>STR NOI:</strong> 5.5%-of-price estimate.<hr><em>Click NOI to enter Awning's gross revenue.</em>")}();const l=document.getElementById("ln-footer");l&&l.classList.toggle("negative",a)},getCurrentPrice:getCurrentPrice,syncUnitsFieldForType:function(e,t){const n=document.querySelector(".units-inline-label");if(n&&(n.textContent="assisted"===e?"beds":"units"),"assisted"===e&&null!=t){const e=document.getElementById("ln-units-input");e&&(e.value=String(t)),y({numberOfUnits:t})}},updateActiveCapDisplay:function(){const r=document.getElementById("prop-cap");if(!r)return;const o=getCurrentPrice()||document.getElementById("prop-price")?.textContent||"",c=i(o);r.textContent=a(f.baseNOI,c);const s=l(f.originalCapRate,f.isUsingEstimatedCapRate),p=`${"<strong>Reported cap rate:</strong> "+(null!=s?`${s}%`:"N/A")}
|
|
1
|
+
import{hasTooltip as e,attachTooltip as t,updateTooltipContent as n}from"../ui/tooltip-manager.js";import{generateDownPaymentTooltipHTML as r,generateCashFlowTooltipHTML as o}from"../financial/tooltip-content-generators.js";import{parseFinancialData as c,parseCashFlowData as s}from"../financial/tooltip-calculations.js";import{parsePriceNumber as i,computeActiveCapDisplay as a,parseReportedCap as l}from"../financial/capRate.js";import{equityPercentFromDebt as p}from"../../financial/calculations.js";import{formatCurrency as u}from"../../financial/formatters.js";import{PROPERTY_TYPE_CONSTANTS as d}from"../../config/property-types.js";const m=["prop-noi","prop-down","prop-net","prop-seller-fi","prop-cocr-30","prop-cocr-15","prop-assignment","prop-dscr","prop-sf","prop-cashflow"];function createRender({ctx:g}){const{state:f,updateState:y}=g;function getCurrentPrice(){if(f.originalPrice&&f.currentPriceDiscount>0){const e=parseFloat(f.originalPrice.replace(/[$,]/g,""))*(1-f.currentPriceDiscount/100);return`$${Math.round(e).toLocaleString()}`}return f.originalPrice}function updatePercentageLabels(){const e=document.querySelector("#prop-down")?.closest(".metric")?.querySelector(".metric-label");e&&(e.textContent=`Down (${f.currentDownPaymentPercent}%)`);const t=document.querySelector("#prop-seller-fi")?.closest(".metric")?.querySelector(".metric-label");t&&(t.textContent=`Seller FI (${f.currentSellerFiPercent}%)`);const n=document.querySelector("#prop-dscr")?.closest(".metric")?.querySelector(".metric-label");n&&(n.textContent=`DSCR (${f.currentDSCRPercent}%)`)}function updateElement(e,t){const n=document.getElementById(e);n&&(n.textContent=t)}return{applyFinancials:function(i){let a=!1;if(i){const l={"prop-noi":i.noi,"prop-down":i.down,"prop-net":i.netToBuyer,"prop-seller-fi":i.sellerFi,"prop-cocr-30":i.cocr30,"prop-cocr-15":i.priceForCOCR15,"prop-assignment":i.assignment,"prop-dscr":i.dscr,"prop-sf":i.sfPayment,"prop-cashflow":i.cashFlow};a=i.rawCashFlow<0;for(const[e,t]of Object.entries(l))updateElement(e,t);setTimeout(()=>{!function(){const o=document.getElementById("prop-down"),s=document.getElementById("prop-price"),i=document.getElementById("prop-noi");if(!o||!s||!i)return;const a=c(s.textContent,i.textContent);if(a){const c=r(a.price,a.noi,f.currentDownPaymentPercent,f.currentDSCRPercent,f.currentSellerFiPercent),s=o.closest(".metric");s&&(e(s)?n(s,c):t(s,c))}}(),function(){const r=document.getElementById("prop-cashflow"),c=document.getElementById("prop-price");if(!r||!c)return;const i=s(c.textContent,r.textContent);if(i){const c=o(i.price,i.monthlyCashFlow),s=r.closest(".metric");if(s){const r=s.querySelector(".metric-label");r&&r.classList.add("has-tooltip"),e(s)?n(s,c):t(s,c)}}}()},100)}else m.forEach(e=>updateElement(e,"N/A"));updatePercentageLabels(),function(){const r="str"===f.currentPropertyType,o=document.getElementById("prop-noi-awning");o&&(o.style.display=r?"inline-block":"none");const c=document.getElementById("prop-noi");if(!c)return;if(c.style.cursor=r?"pointer":"",c.querySelector("input"))return;const s=c.closest(".metric");if(r&&f.cachedStrValue&&Number.isFinite(f.cachedStrValue.value)){c.textContent=`${c.textContent}*`;const r=f.cachedStrValue.value,o=d.STR.NOI_PERCENTAGE,i=`<strong>Manual STR gross (Awning):</strong> ${u(r)}/yr<br>NOI = gross × ${Math.round(100*o)}% = ${u(r*o)}<hr><em>Click NOI to edit; click the label to reset to the estimate.</em>`;s&&(e(s)?n(s,i):(t(s,i),s.querySelector(".metric-label")?.classList.add("has-tooltip")))}else r&&s&&e(s)&&n(s,"<strong>STR NOI:</strong> 5.5%-of-price estimate.<hr><em>Click NOI to enter Awning's gross revenue.</em>")}();const l=document.getElementById("ln-footer");l&&l.classList.toggle("negative",a)},getCurrentPrice:getCurrentPrice,syncUnitsFieldForType:function(e,t){const n=document.querySelector(".units-inline-label");if(n&&(n.textContent="assisted"===e?"beds":"units"),"assisted"===e&&null!=t){const e=document.getElementById("ln-units-input");e&&(e.value=String(t)),y({numberOfUnits:t})}},updateActiveCapDisplay:function(){const r=document.getElementById("prop-cap");if(!r)return;if(r.querySelector("input"))return;const o=getCurrentPrice()||document.getElementById("prop-price")?.textContent||"",c=i(o);r.textContent=a(f.baseNOI,c);const s=l(f.originalCapRate,f.isUsingEstimatedCapRate),p=`${"<strong>Reported cap rate:</strong> "+(null!=s?`${s}%`:"N/A")}<hr><em>Click the cap rate to enter a value; click the label to reset</em>`,u=r.closest(".metric");if(u){const r=u.querySelector(".metric-label");e(u)?n(u,p):(t(u,p),r&&r.classList.add("has-tooltip"))}},updateCapRateLabel:function(){const e=document.querySelector("#prop-cap")?.closest(".metric")?.querySelector(".metric-label");if(e)switch(f.currentPropertyType){case"str":e.textContent="Cap Rate (STR)";break;case"assisted":e.textContent="Cap Rate (Assisted)";break;default:e.textContent="Cap Rate"}},updateElement:updateElement,updateEquityDisplay:function(){const r=document.getElementById("prop-equity");if(!r)return;const o=getCurrentPrice()||document.getElementById("prop-price")?.textContent||"",c=i(o),s=f.cachedDebtBalance,a=null==s,l=p(c,s);r.textContent=`${Math.round(100*l)}%${a?"*":""}`;const fmtUSD=e=>Number.isFinite(Number(e))?`$${Math.round(Number(e)).toLocaleString()}`:"N/A",u=a?"Estimated — no debt data, assuming 100% equity":"scraped"===f.equitySource?"Public records (API)":f.equitySource;let d=`<strong>Debt owing:</strong> ${a?"Unknown":fmtUSD(s)}`;d+=`<br><strong>Source:</strong> ${u}`,f.cachedDebtAddress&&(d+=`<br><strong>Matched:</strong> ${f.cachedDebtAddress}`);const m=Array.isArray(f.cachedMortgages)?f.cachedMortgages:[];m.length&&(d+="<hr>"+m.map(e=>{const t=null!=e.interestRate&&""!==e.interestRate?` @ ${e.interestRate}%`:"";return`<strong>${e.position||"Lien"}:</strong> ${fmtUSD(e.amount)} ${e.loanType||""}${t} (${e.lenderName||"?"})`}).join("<br>"));const g=r.closest(".metric");if(g){const r=g.querySelector(".metric-label");e(g)?n(g,d):(t(g,d),r&&r.classList.add("has-tooltip"))}},updateLeadStatusTooltip:function(r){const o=document.getElementById("prop-lead-status");if(!o)return;let c="No LOI data returned";r&&(c=`\n <strong>Contact:</strong> ${r.contactName} <br>\n ${r.opportunityAddress}\n `);const s=o.closest(".metric");if(s)if(e(s))n(s,c);else{t(s,c);const e=s.querySelector(".metric-label");e&&e.classList.add("has-tooltip")}},updatePercentageLabels:updatePercentageLabels,updatePriceLabel:function(){const e=document.querySelector("#prop-price")?.closest(".metric")?.querySelector(".metric-label");e&&(e.textContent=f.currentPriceDiscount>0?`Price (${f.currentPriceDiscount}%)`:"Price")}}}export{createRender};
|
|
2
2
|
//# sourceMappingURL=render.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"render.js","sources":["../../../src/browser/widget/render.js"],"sourcesContent":["// Render unit: the panel's DOM-painting helpers — price/label updates, the financial-metric\r\n// paint, the active-cap display, and the hover tooltips. Everything here reads ctx.state and\r\n// writes the DOM; the finance math it needs comes from the pure capRate helpers. Extracted\r\n// verbatim from createAnalyzer's render closures (T12 decompose).\r\n\r\nimport { attachTooltip, hasTooltip, updateTooltipContent } from \"../ui/tooltip-manager.js\";\r\nimport {\r\n generateCashFlowTooltipHTML,\r\n generateDownPaymentTooltipHTML,\r\n} from \"../financial/tooltip-content-generators.js\";\r\nimport { parseCashFlowData, parseFinancialData } from \"../financial/tooltip-calculations.js\";\r\nimport { computeActiveCapDisplay, parsePriceNumber, parseReportedCap } from \"../financial/capRate.js\";\r\nimport { equityPercentFromDebt } from \"../../financial/calculations.js\";\r\nimport { formatCurrency } from \"../../financial/formatters.js\";\r\nimport { PROPERTY_TYPE_CONSTANTS } from \"../../config/property-types.js\";\r\n\r\nconst FINANCIAL_ELEMENT_IDS = [\r\n \"prop-noi\", \"prop-down\", \"prop-net\", \"prop-seller-fi\", \"prop-cocr-30\",\r\n \"prop-cocr-15\", \"prop-assignment\", \"prop-dscr\", \"prop-sf\", \"prop-cashflow\",\r\n];\r\n\r\nexport function createRender({ ctx }) {\r\n const { state, updateState } = ctx;\r\n\r\n function getCurrentPrice() {\r\n if (state.originalPrice && state.currentPriceDiscount > 0) {\r\n const numericPrice = parseFloat(state.originalPrice.replace(/[$,]/g, \"\"));\r\n const discountedPrice = numericPrice * (1 - state.currentPriceDiscount / 100);\r\n return `$${Math.round(discountedPrice).toLocaleString()}`;\r\n }\r\n return state.originalPrice;\r\n }\r\n\r\n function updatePriceLabel() {\r\n const priceLabelElement = document.querySelector(\"#prop-price\")?.closest(\".metric\")?.querySelector(\".metric-label\");\r\n if (priceLabelElement) {\r\n priceLabelElement.textContent = state.currentPriceDiscount > 0 ? `Price (${state.currentPriceDiscount}%)` : \"Price\";\r\n }\r\n }\r\n\r\n function updatePercentageLabels() {\r\n const downLabelElement = document.querySelector(\"#prop-down\")?.closest(\".metric\")?.querySelector(\".metric-label\");\r\n if (downLabelElement) downLabelElement.textContent = `Down (${state.currentDownPaymentPercent}%)`;\r\n\r\n const sellerFiLabelElement = document.querySelector(\"#prop-seller-fi\")?.closest(\".metric\")?.querySelector(\".metric-label\");\r\n if (sellerFiLabelElement) sellerFiLabelElement.textContent = `Seller FI (${state.currentSellerFiPercent}%)`;\r\n\r\n const dscrLabelElement = document.querySelector(\"#prop-dscr\")?.closest(\".metric\")?.querySelector(\".metric-label\");\r\n if (dscrLabelElement) dscrLabelElement.textContent = `DSCR (${state.currentDSCRPercent}%)`;\r\n }\r\n\r\n function updateElement(id, value) {\r\n const element = document.getElementById(id);\r\n if (element) element.textContent = value;\r\n }\r\n\r\n function updateCapRateLabel() {\r\n const capLabelElement = document.querySelector(\"#prop-cap\")?.closest(\".metric\")?.querySelector(\".metric-label\");\r\n if (!capLabelElement) return;\r\n switch (state.currentPropertyType) {\r\n case \"str\": capLabelElement.textContent = \"Cap Rate (STR)\"; break;\r\n case \"assisted\": capLabelElement.textContent = \"Cap Rate (Assisted)\"; break;\r\n default: capLabelElement.textContent = \"Cap Rate\"; break;\r\n }\r\n }\r\n\r\n // The panel shows the ACTIVE cap rate = NOI / current price (discount-aware via\r\n // getCurrentPrice), so the displayed cap is always internally consistent with the NOI metric\r\n // — including STR/assisted, where NOI is the type estimate/bedroom value and the listed cap\r\n // never drove it. The REPORTED cap (the scraped value, only when it was a real non-estimated\r\n // cap) is shown on hover; \"N/A\" when none was reported. When the cap is an estimate the\r\n // tooltip also keeps the click-to-cycle hint (clicking the cap is a manual NOI override).\r\n function updateActiveCapDisplay() {\r\n const capElement = document.getElementById(\"prop-cap\");\r\n if (!capElement) return;\r\n\r\n const priceText = getCurrentPrice() || document.getElementById(\"prop-price\")?.textContent || \"\";\r\n const price = parsePriceNumber(priceText);\r\n capElement.textContent = computeActiveCapDisplay(state.baseNOI, price);\r\n\r\n const reported = parseReportedCap(state.originalCapRate, state.isUsingEstimatedCapRate);\r\n const reportedLine = `<strong>Reported cap rate:</strong> ${reported != null ? `${reported}%` : \"N/A\"}`;\r\n const cycleHint = state.isUsingEstimatedCapRate\r\n ? \"<hr><em>Click the cap rate to increase by 1%; click the label to reset</em>\"\r\n : \"\";\r\n const tooltipContent = `${reportedLine}${cycleHint}`;\r\n\r\n const metric = capElement.closest(\".metric\");\r\n if (metric) {\r\n const label = metric.querySelector(\".metric-label\");\r\n if (!hasTooltip(metric)) {\r\n attachTooltip(metric, tooltipContent);\r\n if (label) label.classList.add(\"has-tooltip\");\r\n } else {\r\n updateTooltipContent(metric, tooltipContent);\r\n }\r\n }\r\n }\r\n\r\n // Equity is DERIVED, not fetched: (price - debt) / price against the current (discount-aware)\r\n // price, so it recomputes on every price edit. No debt figure => 100% (\"estimated\", marked *).\r\n // The hover reveals the $ debt, the recorded liens, the matched address, and how it was acquired.\r\n function updateEquityDisplay() {\r\n const equityElement = document.getElementById(\"prop-equity\");\r\n if (!equityElement) return;\r\n\r\n const priceText = getCurrentPrice() || document.getElementById(\"prop-price\")?.textContent || \"\";\r\n const price = parsePriceNumber(priceText);\r\n const debt = state.cachedDebtBalance;\r\n const estimated = debt === null || debt === undefined;\r\n const equity = equityPercentFromDebt(price, debt);\r\n equityElement.textContent = `${Math.round(equity * 100)}%${estimated ? \"*\" : \"\"}`;\r\n\r\n const fmtUSD = (n) => (Number.isFinite(Number(n)) ? `$${Math.round(Number(n)).toLocaleString()}` : \"N/A\");\r\n const sourceLabel = estimated\r\n ? \"Estimated — no debt data, assuming 100% equity\"\r\n : (state.equitySource === \"scraped\" ? \"Public records (API)\" : state.equitySource);\r\n\r\n let tooltip = `<strong>Debt owing:</strong> ${estimated ? \"Unknown\" : fmtUSD(debt)}`;\r\n tooltip += `<br><strong>Source:</strong> ${sourceLabel}`;\r\n if (state.cachedDebtAddress) tooltip += `<br><strong>Matched:</strong> ${state.cachedDebtAddress}`;\r\n\r\n const mortgages = Array.isArray(state.cachedMortgages) ? state.cachedMortgages : [];\r\n if (mortgages.length) {\r\n tooltip += \"<hr>\" + mortgages.map((m) => {\r\n const rate = (m.interestRate != null && m.interestRate !== \"\") ? ` @ ${m.interestRate}%` : \"\";\r\n return `<strong>${m.position || \"Lien\"}:</strong> ${fmtUSD(m.amount)} ${m.loanType || \"\"}${rate} (${m.lenderName || \"?\"})`;\r\n }).join(\"<br>\");\r\n }\r\n\r\n const metric = equityElement.closest(\".metric\");\r\n if (metric) {\r\n const label = metric.querySelector(\".metric-label\");\r\n if (!hasTooltip(metric)) {\r\n attachTooltip(metric, tooltip);\r\n if (label) label.classList.add(\"has-tooltip\");\r\n } else {\r\n updateTooltipContent(metric, tooltip);\r\n }\r\n }\r\n }\r\n\r\n function syncUnitsFieldForType(propertyType, bedroomCount) {\r\n const label = document.querySelector(\".units-inline-label\");\r\n if (label) label.textContent = propertyType === \"assisted\" ? \"beds\" : \"units\";\r\n if (propertyType === \"assisted\" && bedroomCount != null) {\r\n const input = document.getElementById(\"ln-units-input\");\r\n if (input) input.value = String(bedroomCount);\r\n updateState({ numberOfUnits: bedroomCount });\r\n }\r\n }\r\n\r\n function updateLeadStatusTooltip(loiData) {\r\n const leadElement = document.getElementById(\"prop-lead-status\");\r\n if (!leadElement) return;\r\n let tooltipContent = \"No LOI data returned\";\r\n if (loiData) {\r\n tooltipContent = `\r\n <strong>Contact:</strong> ${loiData.contactName} <br>\r\n ${loiData.opportunityAddress}\r\n `;\r\n }\r\n const metric = leadElement.closest(\".metric\");\r\n if (metric) {\r\n if (!hasTooltip(metric)) {\r\n attachTooltip(metric, tooltipContent);\r\n const label = metric.querySelector(\".metric-label\");\r\n if (label) label.classList.add(\"has-tooltip\");\r\n } else {\r\n updateTooltipContent(metric, tooltipContent);\r\n }\r\n }\r\n }\r\n\r\n function updateDownHoverTooltip() {\r\n const downElement = document.getElementById(\"prop-down\");\r\n const priceElement = document.getElementById(\"prop-price\");\r\n const noiElement = document.getElementById(\"prop-noi\");\r\n if (!downElement || !priceElement || !noiElement) return;\r\n\r\n const financialData = parseFinancialData(priceElement.textContent, noiElement.textContent);\r\n if (financialData) {\r\n const tooltipContent = generateDownPaymentTooltipHTML(\r\n financialData.price,\r\n financialData.noi,\r\n state.currentDownPaymentPercent,\r\n state.currentDSCRPercent,\r\n state.currentSellerFiPercent\r\n );\r\n const metric = downElement.closest(\".metric\");\r\n if (metric) {\r\n if (!hasTooltip(metric)) attachTooltip(metric, tooltipContent);\r\n else updateTooltipContent(metric, tooltipContent);\r\n }\r\n }\r\n }\r\n\r\n function updateCashFlowHoverTooltip() {\r\n const cashFlowElement = document.getElementById(\"prop-cashflow\");\r\n const priceElement = document.getElementById(\"prop-price\");\r\n if (!cashFlowElement || !priceElement) return;\r\n\r\n const cashFlowData = parseCashFlowData(priceElement.textContent, cashFlowElement.textContent);\r\n if (cashFlowData) {\r\n const tooltipContent = generateCashFlowTooltipHTML(cashFlowData.price, cashFlowData.monthlyCashFlow);\r\n const metric = cashFlowElement.closest(\".metric\");\r\n if (metric) {\r\n const label = metric.querySelector(\".metric-label\");\r\n if (label) label.classList.add(\"has-tooltip\");\r\n if (!hasTooltip(metric)) attachTooltip(metric, tooltipContent);\r\n else updateTooltipContent(metric, tooltipContent);\r\n }\r\n }\r\n }\r\n\r\n // STR affordances: the Awning link and the click-to-edit NOI cell exist only in STR mode.\r\n // When a manual STR gross is active (cachedStrValue), mark the painted NOI with a trailing *\r\n // and a source tooltip — mirroring the cap-rate/equity \"*\" convention. Runs after the metric\r\n // paint so the * is appended to the freshly-set NOI text (and re-applied on every recalc).\r\n function updateStrAffordances() {\r\n const isStr = state.currentPropertyType === \"str\";\r\n\r\n const awningLink = document.getElementById(\"prop-noi-awning\");\r\n if (awningLink) awningLink.style.display = isStr ? \"inline-block\" : \"none\";\r\n\r\n const noiElement = document.getElementById(\"prop-noi\");\r\n if (!noiElement) return;\r\n noiElement.style.cursor = isStr ? \"pointer\" : \"\";\r\n if (noiElement.querySelector(\"input\")) return;\r\n\r\n const metric = noiElement.closest(\".metric\");\r\n const manual = isStr && state.cachedStrValue && Number.isFinite(state.cachedStrValue.value);\r\n\r\n if (manual) {\r\n noiElement.textContent = `${noiElement.textContent}*`;\r\n const gross = state.cachedStrValue.value;\r\n const pct = PROPERTY_TYPE_CONSTANTS.STR.NOI_PERCENTAGE;\r\n const tip = `<strong>Manual STR gross (Awning):</strong> ${formatCurrency(gross)}/yr`\r\n + `<br>NOI = gross × ${Math.round(pct * 100)}% = ${formatCurrency(gross * pct)}`\r\n + `<hr><em>Click NOI to edit; click the label to reset to the estimate.</em>`;\r\n if (metric) {\r\n if (!hasTooltip(metric)) {\r\n attachTooltip(metric, tip);\r\n metric.querySelector(\".metric-label\")?.classList.add(\"has-tooltip\");\r\n } else {\r\n updateTooltipContent(metric, tip);\r\n }\r\n }\r\n } else if (isStr && metric && hasTooltip(metric)) {\r\n updateTooltipContent(metric, \"<strong>STR NOI:</strong> 5.5%-of-price estimate.<hr><em>Click NOI to enter Awning's gross revenue.</em>\");\r\n }\r\n }\r\n\r\n function applyFinancials(financials) {\r\n let isCashFlowNegative = false;\r\n if (financials) {\r\n const elements = {\r\n \"prop-noi\": financials.noi,\r\n \"prop-down\": financials.down,\r\n \"prop-net\": financials.netToBuyer,\r\n \"prop-seller-fi\": financials.sellerFi,\r\n \"prop-cocr-30\": financials.cocr30,\r\n \"prop-cocr-15\": financials.priceForCOCR15,\r\n \"prop-assignment\": financials.assignment,\r\n \"prop-dscr\": financials.dscr,\r\n \"prop-sf\": financials.sfPayment,\r\n \"prop-cashflow\": financials.cashFlow,\r\n };\r\n isCashFlowNegative = financials.rawCashFlow < 0;\r\n for (const [id, value] of Object.entries(elements)) updateElement(id, value);\r\n setTimeout(() => {\r\n updateDownHoverTooltip();\r\n updateCashFlowHoverTooltip();\r\n }, 100);\r\n } else {\r\n FINANCIAL_ELEMENT_IDS.forEach((id) => updateElement(id, \"N/A\"));\r\n }\r\n\r\n updatePercentageLabels();\r\n updateStrAffordances();\r\n const footer = document.getElementById(\"ln-footer\");\r\n if (footer) footer.classList.toggle(\"negative\", isCashFlowNegative);\r\n }\r\n\r\n return {\r\n applyFinancials,\r\n getCurrentPrice,\r\n syncUnitsFieldForType,\r\n updateActiveCapDisplay,\r\n updateCapRateLabel,\r\n updateElement,\r\n updateEquityDisplay,\r\n updateLeadStatusTooltip,\r\n updatePercentageLabels,\r\n updatePriceLabel,\r\n };\r\n}\r\n"],"names":["FINANCIAL_ELEMENT_IDS","createRender","ctx","state","updateState","getCurrentPrice","originalPrice","currentPriceDiscount","discountedPrice","parseFloat","replace","Math","round","toLocaleString","updatePercentageLabels","downLabelElement","document","querySelector","closest","textContent","currentDownPaymentPercent","sellerFiLabelElement","currentSellerFiPercent","dscrLabelElement","currentDSCRPercent","updateElement","id","value","element","getElementById","applyFinancials","financials","isCashFlowNegative","elements","noi","down","netToBuyer","sellerFi","cocr30","priceForCOCR15","assignment","dscr","sfPayment","cashFlow","rawCashFlow","Object","entries","setTimeout","downElement","priceElement","noiElement","financialData","parseFinancialData","tooltipContent","generateDownPaymentTooltipHTML","price","metric","hasTooltip","updateTooltipContent","attachTooltip","updateDownHoverTooltip","cashFlowElement","cashFlowData","parseCashFlowData","generateCashFlowTooltipHTML","monthlyCashFlow","label","classList","add","updateCashFlowHoverTooltip","forEach","isStr","currentPropertyType","awningLink","style","display","cursor","cachedStrValue","Number","isFinite","gross","pct","PROPERTY_TYPE_CONSTANTS","STR","NOI_PERCENTAGE","tip","formatCurrency","updateStrAffordances","footer","toggle","syncUnitsFieldForType","propertyType","bedroomCount","input","String","numberOfUnits","updateActiveCapDisplay","capElement","priceText","parsePriceNumber","computeActiveCapDisplay","baseNOI","reported","parseReportedCap","originalCapRate","isUsingEstimatedCapRate","updateCapRateLabel","capLabelElement","updateEquityDisplay","equityElement","debt","cachedDebtBalance","estimated","equity","equityPercentFromDebt","fmtUSD","n","sourceLabel","equitySource","tooltip","cachedDebtAddress","mortgages","Array","isArray","cachedMortgages","length","map","m","rate","interestRate","position","amount","loanType","lenderName","join","updateLeadStatusTooltip","loiData","leadElement","contactName","opportunityAddress","updatePriceLabel","priceLabelElement"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"render.js","sources":["../../../src/browser/widget/render.js"],"sourcesContent":["// Render unit: the panel's DOM-painting helpers — price/label updates, the financial-metric\r\n// paint, the active-cap display, and the hover tooltips. Everything here reads ctx.state and\r\n// writes the DOM; the finance math it needs comes from the pure capRate helpers. Extracted\r\n// verbatim from createAnalyzer's render closures (T12 decompose).\r\n\r\nimport { attachTooltip, hasTooltip, updateTooltipContent } from \"../ui/tooltip-manager.js\";\r\nimport {\r\n generateCashFlowTooltipHTML,\r\n generateDownPaymentTooltipHTML,\r\n} from \"../financial/tooltip-content-generators.js\";\r\nimport { parseCashFlowData, parseFinancialData } from \"../financial/tooltip-calculations.js\";\r\nimport { computeActiveCapDisplay, parsePriceNumber, parseReportedCap } from \"../financial/capRate.js\";\r\nimport { equityPercentFromDebt } from \"../../financial/calculations.js\";\r\nimport { formatCurrency } from \"../../financial/formatters.js\";\r\nimport { PROPERTY_TYPE_CONSTANTS } from \"../../config/property-types.js\";\r\n\r\nconst FINANCIAL_ELEMENT_IDS = [\r\n \"prop-noi\", \"prop-down\", \"prop-net\", \"prop-seller-fi\", \"prop-cocr-30\",\r\n \"prop-cocr-15\", \"prop-assignment\", \"prop-dscr\", \"prop-sf\", \"prop-cashflow\",\r\n];\r\n\r\nexport function createRender({ ctx }) {\r\n const { state, updateState } = ctx;\r\n\r\n function getCurrentPrice() {\r\n if (state.originalPrice && state.currentPriceDiscount > 0) {\r\n const numericPrice = parseFloat(state.originalPrice.replace(/[$,]/g, \"\"));\r\n const discountedPrice = numericPrice * (1 - state.currentPriceDiscount / 100);\r\n return `$${Math.round(discountedPrice).toLocaleString()}`;\r\n }\r\n return state.originalPrice;\r\n }\r\n\r\n function updatePriceLabel() {\r\n const priceLabelElement = document.querySelector(\"#prop-price\")?.closest(\".metric\")?.querySelector(\".metric-label\");\r\n if (priceLabelElement) {\r\n priceLabelElement.textContent = state.currentPriceDiscount > 0 ? `Price (${state.currentPriceDiscount}%)` : \"Price\";\r\n }\r\n }\r\n\r\n function updatePercentageLabels() {\r\n const downLabelElement = document.querySelector(\"#prop-down\")?.closest(\".metric\")?.querySelector(\".metric-label\");\r\n if (downLabelElement) downLabelElement.textContent = `Down (${state.currentDownPaymentPercent}%)`;\r\n\r\n const sellerFiLabelElement = document.querySelector(\"#prop-seller-fi\")?.closest(\".metric\")?.querySelector(\".metric-label\");\r\n if (sellerFiLabelElement) sellerFiLabelElement.textContent = `Seller FI (${state.currentSellerFiPercent}%)`;\r\n\r\n const dscrLabelElement = document.querySelector(\"#prop-dscr\")?.closest(\".metric\")?.querySelector(\".metric-label\");\r\n if (dscrLabelElement) dscrLabelElement.textContent = `DSCR (${state.currentDSCRPercent}%)`;\r\n }\r\n\r\n function updateElement(id, value) {\r\n const element = document.getElementById(id);\r\n if (element) element.textContent = value;\r\n }\r\n\r\n function updateCapRateLabel() {\r\n const capLabelElement = document.querySelector(\"#prop-cap\")?.closest(\".metric\")?.querySelector(\".metric-label\");\r\n if (!capLabelElement) return;\r\n switch (state.currentPropertyType) {\r\n case \"str\": capLabelElement.textContent = \"Cap Rate (STR)\"; break;\r\n case \"assisted\": capLabelElement.textContent = \"Cap Rate (Assisted)\"; break;\r\n default: capLabelElement.textContent = \"Cap Rate\"; break;\r\n }\r\n }\r\n\r\n // The panel shows the ACTIVE cap rate = NOI / current price (discount-aware via\r\n // getCurrentPrice), so the displayed cap is always internally consistent with the NOI metric\r\n // — including STR/assisted, where NOI is the type estimate/bedroom value and the listed cap\r\n // never drove it. The REPORTED cap (the scraped value, only when it was a real non-estimated\r\n // cap) is shown on hover; \"N/A\" when none was reported. When the cap is an estimate the\r\n // tooltip also keeps the click-to-cycle hint (clicking the cap is a manual NOI override).\r\n function updateActiveCapDisplay() {\r\n const capElement = document.getElementById(\"prop-cap\");\r\n if (!capElement) return;\r\n // Don't clobber an in-progress manual cap edit (the inline input lives in this cell).\r\n if (capElement.querySelector(\"input\")) return;\r\n\r\n const priceText = getCurrentPrice() || document.getElementById(\"prop-price\")?.textContent || \"\";\r\n const price = parsePriceNumber(priceText);\r\n capElement.textContent = computeActiveCapDisplay(state.baseNOI, price);\r\n\r\n const reported = parseReportedCap(state.originalCapRate, state.isUsingEstimatedCapRate);\r\n const reportedLine = `<strong>Reported cap rate:</strong> ${reported != null ? `${reported}%` : \"N/A\"}`;\r\n const editHint = \"<hr><em>Click the cap rate to enter a value; click the label to reset</em>\";\r\n const tooltipContent = `${reportedLine}${editHint}`;\r\n\r\n const metric = capElement.closest(\".metric\");\r\n if (metric) {\r\n const label = metric.querySelector(\".metric-label\");\r\n if (!hasTooltip(metric)) {\r\n attachTooltip(metric, tooltipContent);\r\n if (label) label.classList.add(\"has-tooltip\");\r\n } else {\r\n updateTooltipContent(metric, tooltipContent);\r\n }\r\n }\r\n }\r\n\r\n // Equity is DERIVED, not fetched: (price - debt) / price against the current (discount-aware)\r\n // price, so it recomputes on every price edit. No debt figure => 100% (\"estimated\", marked *).\r\n // The hover reveals the $ debt, the recorded liens, the matched address, and how it was acquired.\r\n function updateEquityDisplay() {\r\n const equityElement = document.getElementById(\"prop-equity\");\r\n if (!equityElement) return;\r\n\r\n const priceText = getCurrentPrice() || document.getElementById(\"prop-price\")?.textContent || \"\";\r\n const price = parsePriceNumber(priceText);\r\n const debt = state.cachedDebtBalance;\r\n const estimated = debt === null || debt === undefined;\r\n const equity = equityPercentFromDebt(price, debt);\r\n equityElement.textContent = `${Math.round(equity * 100)}%${estimated ? \"*\" : \"\"}`;\r\n\r\n const fmtUSD = (n) => (Number.isFinite(Number(n)) ? `$${Math.round(Number(n)).toLocaleString()}` : \"N/A\");\r\n const sourceLabel = estimated\r\n ? \"Estimated — no debt data, assuming 100% equity\"\r\n : (state.equitySource === \"scraped\" ? \"Public records (API)\" : state.equitySource);\r\n\r\n let tooltip = `<strong>Debt owing:</strong> ${estimated ? \"Unknown\" : fmtUSD(debt)}`;\r\n tooltip += `<br><strong>Source:</strong> ${sourceLabel}`;\r\n if (state.cachedDebtAddress) tooltip += `<br><strong>Matched:</strong> ${state.cachedDebtAddress}`;\r\n\r\n const mortgages = Array.isArray(state.cachedMortgages) ? state.cachedMortgages : [];\r\n if (mortgages.length) {\r\n tooltip += \"<hr>\" + mortgages.map((m) => {\r\n const rate = (m.interestRate != null && m.interestRate !== \"\") ? ` @ ${m.interestRate}%` : \"\";\r\n return `<strong>${m.position || \"Lien\"}:</strong> ${fmtUSD(m.amount)} ${m.loanType || \"\"}${rate} (${m.lenderName || \"?\"})`;\r\n }).join(\"<br>\");\r\n }\r\n\r\n const metric = equityElement.closest(\".metric\");\r\n if (metric) {\r\n const label = metric.querySelector(\".metric-label\");\r\n if (!hasTooltip(metric)) {\r\n attachTooltip(metric, tooltip);\r\n if (label) label.classList.add(\"has-tooltip\");\r\n } else {\r\n updateTooltipContent(metric, tooltip);\r\n }\r\n }\r\n }\r\n\r\n function syncUnitsFieldForType(propertyType, bedroomCount) {\r\n const label = document.querySelector(\".units-inline-label\");\r\n if (label) label.textContent = propertyType === \"assisted\" ? \"beds\" : \"units\";\r\n if (propertyType === \"assisted\" && bedroomCount != null) {\r\n const input = document.getElementById(\"ln-units-input\");\r\n if (input) input.value = String(bedroomCount);\r\n updateState({ numberOfUnits: bedroomCount });\r\n }\r\n }\r\n\r\n function updateLeadStatusTooltip(loiData) {\r\n const leadElement = document.getElementById(\"prop-lead-status\");\r\n if (!leadElement) return;\r\n let tooltipContent = \"No LOI data returned\";\r\n if (loiData) {\r\n tooltipContent = `\r\n <strong>Contact:</strong> ${loiData.contactName} <br>\r\n ${loiData.opportunityAddress}\r\n `;\r\n }\r\n const metric = leadElement.closest(\".metric\");\r\n if (metric) {\r\n if (!hasTooltip(metric)) {\r\n attachTooltip(metric, tooltipContent);\r\n const label = metric.querySelector(\".metric-label\");\r\n if (label) label.classList.add(\"has-tooltip\");\r\n } else {\r\n updateTooltipContent(metric, tooltipContent);\r\n }\r\n }\r\n }\r\n\r\n function updateDownHoverTooltip() {\r\n const downElement = document.getElementById(\"prop-down\");\r\n const priceElement = document.getElementById(\"prop-price\");\r\n const noiElement = document.getElementById(\"prop-noi\");\r\n if (!downElement || !priceElement || !noiElement) return;\r\n\r\n const financialData = parseFinancialData(priceElement.textContent, noiElement.textContent);\r\n if (financialData) {\r\n const tooltipContent = generateDownPaymentTooltipHTML(\r\n financialData.price,\r\n financialData.noi,\r\n state.currentDownPaymentPercent,\r\n state.currentDSCRPercent,\r\n state.currentSellerFiPercent\r\n );\r\n const metric = downElement.closest(\".metric\");\r\n if (metric) {\r\n if (!hasTooltip(metric)) attachTooltip(metric, tooltipContent);\r\n else updateTooltipContent(metric, tooltipContent);\r\n }\r\n }\r\n }\r\n\r\n function updateCashFlowHoverTooltip() {\r\n const cashFlowElement = document.getElementById(\"prop-cashflow\");\r\n const priceElement = document.getElementById(\"prop-price\");\r\n if (!cashFlowElement || !priceElement) return;\r\n\r\n const cashFlowData = parseCashFlowData(priceElement.textContent, cashFlowElement.textContent);\r\n if (cashFlowData) {\r\n const tooltipContent = generateCashFlowTooltipHTML(cashFlowData.price, cashFlowData.monthlyCashFlow);\r\n const metric = cashFlowElement.closest(\".metric\");\r\n if (metric) {\r\n const label = metric.querySelector(\".metric-label\");\r\n if (label) label.classList.add(\"has-tooltip\");\r\n if (!hasTooltip(metric)) attachTooltip(metric, tooltipContent);\r\n else updateTooltipContent(metric, tooltipContent);\r\n }\r\n }\r\n }\r\n\r\n // STR affordances: the Awning link and the click-to-edit NOI cell exist only in STR mode.\r\n // When a manual STR gross is active (cachedStrValue), mark the painted NOI with a trailing *\r\n // and a source tooltip — mirroring the cap-rate/equity \"*\" convention. Runs after the metric\r\n // paint so the * is appended to the freshly-set NOI text (and re-applied on every recalc).\r\n function updateStrAffordances() {\r\n const isStr = state.currentPropertyType === \"str\";\r\n\r\n const awningLink = document.getElementById(\"prop-noi-awning\");\r\n if (awningLink) awningLink.style.display = isStr ? \"inline-block\" : \"none\";\r\n\r\n const noiElement = document.getElementById(\"prop-noi\");\r\n if (!noiElement) return;\r\n noiElement.style.cursor = isStr ? \"pointer\" : \"\";\r\n if (noiElement.querySelector(\"input\")) return;\r\n\r\n const metric = noiElement.closest(\".metric\");\r\n const manual = isStr && state.cachedStrValue && Number.isFinite(state.cachedStrValue.value);\r\n\r\n if (manual) {\r\n noiElement.textContent = `${noiElement.textContent}*`;\r\n const gross = state.cachedStrValue.value;\r\n const pct = PROPERTY_TYPE_CONSTANTS.STR.NOI_PERCENTAGE;\r\n const tip = `<strong>Manual STR gross (Awning):</strong> ${formatCurrency(gross)}/yr`\r\n + `<br>NOI = gross × ${Math.round(pct * 100)}% = ${formatCurrency(gross * pct)}`\r\n + `<hr><em>Click NOI to edit; click the label to reset to the estimate.</em>`;\r\n if (metric) {\r\n if (!hasTooltip(metric)) {\r\n attachTooltip(metric, tip);\r\n metric.querySelector(\".metric-label\")?.classList.add(\"has-tooltip\");\r\n } else {\r\n updateTooltipContent(metric, tip);\r\n }\r\n }\r\n } else if (isStr && metric && hasTooltip(metric)) {\r\n updateTooltipContent(metric, \"<strong>STR NOI:</strong> 5.5%-of-price estimate.<hr><em>Click NOI to enter Awning's gross revenue.</em>\");\r\n }\r\n }\r\n\r\n function applyFinancials(financials) {\r\n let isCashFlowNegative = false;\r\n if (financials) {\r\n const elements = {\r\n \"prop-noi\": financials.noi,\r\n \"prop-down\": financials.down,\r\n \"prop-net\": financials.netToBuyer,\r\n \"prop-seller-fi\": financials.sellerFi,\r\n \"prop-cocr-30\": financials.cocr30,\r\n \"prop-cocr-15\": financials.priceForCOCR15,\r\n \"prop-assignment\": financials.assignment,\r\n \"prop-dscr\": financials.dscr,\r\n \"prop-sf\": financials.sfPayment,\r\n \"prop-cashflow\": financials.cashFlow,\r\n };\r\n isCashFlowNegative = financials.rawCashFlow < 0;\r\n for (const [id, value] of Object.entries(elements)) updateElement(id, value);\r\n setTimeout(() => {\r\n updateDownHoverTooltip();\r\n updateCashFlowHoverTooltip();\r\n }, 100);\r\n } else {\r\n FINANCIAL_ELEMENT_IDS.forEach((id) => updateElement(id, \"N/A\"));\r\n }\r\n\r\n updatePercentageLabels();\r\n updateStrAffordances();\r\n const footer = document.getElementById(\"ln-footer\");\r\n if (footer) footer.classList.toggle(\"negative\", isCashFlowNegative);\r\n }\r\n\r\n return {\r\n applyFinancials,\r\n getCurrentPrice,\r\n syncUnitsFieldForType,\r\n updateActiveCapDisplay,\r\n updateCapRateLabel,\r\n updateElement,\r\n updateEquityDisplay,\r\n updateLeadStatusTooltip,\r\n updatePercentageLabels,\r\n updatePriceLabel,\r\n };\r\n}\r\n"],"names":["FINANCIAL_ELEMENT_IDS","createRender","ctx","state","updateState","getCurrentPrice","originalPrice","currentPriceDiscount","discountedPrice","parseFloat","replace","Math","round","toLocaleString","updatePercentageLabels","downLabelElement","document","querySelector","closest","textContent","currentDownPaymentPercent","sellerFiLabelElement","currentSellerFiPercent","dscrLabelElement","currentDSCRPercent","updateElement","id","value","element","getElementById","applyFinancials","financials","isCashFlowNegative","elements","noi","down","netToBuyer","sellerFi","cocr30","priceForCOCR15","assignment","dscr","sfPayment","cashFlow","rawCashFlow","Object","entries","setTimeout","downElement","priceElement","noiElement","financialData","parseFinancialData","tooltipContent","generateDownPaymentTooltipHTML","price","metric","hasTooltip","updateTooltipContent","attachTooltip","updateDownHoverTooltip","cashFlowElement","cashFlowData","parseCashFlowData","generateCashFlowTooltipHTML","monthlyCashFlow","label","classList","add","updateCashFlowHoverTooltip","forEach","isStr","currentPropertyType","awningLink","style","display","cursor","cachedStrValue","Number","isFinite","gross","pct","PROPERTY_TYPE_CONSTANTS","STR","NOI_PERCENTAGE","tip","formatCurrency","updateStrAffordances","footer","toggle","syncUnitsFieldForType","propertyType","bedroomCount","input","String","numberOfUnits","updateActiveCapDisplay","capElement","priceText","parsePriceNumber","computeActiveCapDisplay","baseNOI","reported","parseReportedCap","originalCapRate","isUsingEstimatedCapRate","updateCapRateLabel","capLabelElement","updateEquityDisplay","equityElement","debt","cachedDebtBalance","estimated","equity","equityPercentFromDebt","fmtUSD","n","sourceLabel","equitySource","tooltip","cachedDebtAddress","mortgages","Array","isArray","cachedMortgages","length","map","m","rate","interestRate","position","amount","loanType","lenderName","join","updateLeadStatusTooltip","loiData","leadElement","contactName","opportunityAddress","updatePriceLabel","priceLabelElement"],"mappings":"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"}
|