@archerjessop/utilities 7.11.0 → 7.12.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.
@@ -1,2 +1,2 @@
1
- import{updateTooltipContent as t,attachTooltip as e,removeTooltip as n}from"./tooltip-manager.js";import{generateCapRateTooltipHTML as r,generateDownPaymentTooltipHTML as c,generatePriceTooltipHTML as a}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:r}=e;t.addEventListener("click",function(t){t.preventDefault(),t.stopPropagation(),n.currentPriceDiscount>0?r({currentPriceDiscount:0}):r({currentPriceDiscount:15});const c=document.getElementById("prop-price");c&&(c.textContent=e.getCurrentPrice()),e.updatePriceLabel(),e.recalculateFinancials(),updateDiscountButtonText(n)})}function setupPriceClickHandler(n,r,c){if(!n||!r)return;if("true"===n.dataset.handlerAttached)return;n.dataset.handlerAttached="true";const{state:i,updateState:o}=c,s=n.closest(".metric");if(n.addEventListener("click",function(e){e.preventDefault(),e.stopPropagation();let r=10*Math.floor(i.currentPriceDiscount/10)+10;r>50&&(r=0),o({currentPriceDiscount:r});const u=c.getCurrentPrice();if(n.textContent=u,c.updatePriceLabel(),c.recalculateFinancials(),updateDiscountButtonText(i),s){const e=a(i.currentPriceDiscount);t(s,e)}}),r.addEventListener("click",function(e){e.preventDefault(),e.stopPropagation(),o({currentPriceDiscount:0});const r=i.originalPrice;if(n.textContent=r,c.updatePriceLabel(),c.recalculateFinancials(),updateDiscountButtonText(i),s){const e=a(i.currentPriceDiscount);t(s,e)}}),s){const t=a(i.currentPriceDiscount);e(s,t),r.classList.add("has-tooltip")}n.style.cursor="pointer",r.style.cursor="pointer"}function setupCapRateClickHandler(n,c,a){if(!n||!c)return;const{state:o,updateState:s}=a;if(!o.isUsingEstimatedCapRate)return;if("true"===n.dataset.handlerAttached)return;n.dataset.handlerAttached="true";const u=n.closest(".metric");if(n.addEventListener("click",function(e){e.preventDefault(),e.stopPropagation();let c=o.currentEstimatedCapRate+1;if(c>20&&(c=5),s({currentEstimatedCapRate:c,capManuallySet:!0}),n.textContent=`${c}%*`,a.recalculateFinancials(),u){const e=r(o.isUsingEstimatedCapRate);e&&t(u,e)}}),c.addEventListener("click",function(e){e.preventDefault(),e.stopPropagation();const c=o.originalEstimatedCapRate||i.DEFAULT_CAP_RATE;if(s({currentEstimatedCapRate:c,capManuallySet:!1,baseNOI:null}),n.textContent=`${c}%*`,a.recalculateFinancials(),u){const e=r(o.isUsingEstimatedCapRate);e&&t(u,e)}}),u){const t=r(o.isUsingEstimatedCapRate);t&&(e(u,t),c.classList.add("has-tooltip"))}n.style.cursor="pointer",c.style.cursor="pointer"}function setupDownPaymentClickHandler(e,r,a){if(!e||!r)return;if("true"===e.dataset.handlerAttached)return;e.dataset.handlerAttached="true";const{state:o,updateState:s}=a,u=e.closest(".metric");e.addEventListener("click",function(e){e.preventDefault(),e.stopPropagation();let r=o.currentDownPaymentPercent-10,i=o.currentDSCRPercent-10,l=o.currentSellerFiPercent+10;r<0&&(r=60,i=70,l=40),s({currentDownPaymentPercent:r,currentDSCRPercent:i,currentSellerFiPercent:l}),a.updatePercentageLabels(),a.recalculateFinancials(),setTimeout(()=>{const e=document.getElementById("prop-price"),r=document.getElementById("prop-noi");if(e&&r&&u){const a=e.textContent.match(/[\d,]+/),i=r.textContent.match(/[\d,.]+/);if(a&&i){const e=parseFloat(a[0].replace(/,/g,""));let s=parseFloat(i[0].replace(/,/g,""));r.textContent.includes("K")&&(s*=1e3),r.textContent.includes("M")&&(s*=1e6),n(u),setTimeout(()=>{const n=c(e,s,o.currentDownPaymentPercent,o.currentDSCRPercent,o.currentSellerFiPercent,o.currentInterestRateType);t(u,n)},50)}}},100)}),r.addEventListener("click",function(e){e.preventDefault(),e.stopPropagation(),s({currentDownPaymentPercent:100*i.SELLER_FI_DOWN_PAYMENT,currentDSCRPercent:100*i.DEFAULT_DSCR_PERCENTAGE,currentSellerFiPercent:100*i.SELLER_FI_CARRY}),a.updatePercentageLabels(),a.recalculateFinancials(),setTimeout(()=>{const e=document.getElementById("prop-price"),r=document.getElementById("prop-noi");if(e&&r&&u){const a=e.textContent.match(/[\d,]+/),i=r.textContent.match(/[\d,.]+/);if(a&&i){const e=parseFloat(a[0].replace(/,/g,""));let s=parseFloat(i[0].replace(/,/g,""));r.textContent.includes("K")&&(s*=1e3),r.textContent.includes("M")&&(s*=1e6),n(u),setTimeout(()=>{const n=c(e,s,o.currentDownPaymentPercent,o.currentDSCRPercent,o.currentSellerFiPercent,o.currentInterestRateType);t(u,n)},50)}}},100)}),u&&r&&r.classList.add("has-tooltip"),e.style.cursor="pointer",r.style.cursor="pointer"}export{setupCapRateClickHandler,setupDiscountButtonHandler,setupDownPaymentClickHandler,setupPriceClickHandler};
1
+ import{updateTooltipContent as t,attachTooltip as e,removeTooltip as n}from"./tooltip-manager.js";import{generateCapRateTooltipHTML as r,generateDownPaymentTooltipHTML as a,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:r}=e;t.addEventListener("click",function(t){t.preventDefault(),t.stopPropagation(),n.currentPriceDiscount>0?r({currentPriceDiscount:0}):r({currentPriceDiscount:15});const a=document.getElementById("prop-price");a&&(a.textContent=e.getCurrentPrice()),e.updatePriceLabel(),e.recalculateFinancials(),updateDiscountButtonText(n)})}function setupPriceClickHandler(n,r,a){if(!n||!r)return;if("true"===n.dataset.handlerAttached)return;n.dataset.handlerAttached="true";const{state:i,updateState:o}=a,s=n.closest(".metric");if(n.addEventListener("click",function(e){e.preventDefault(),e.stopPropagation();let r=10*Math.floor(i.currentPriceDiscount/10)+10;r>50&&(r=0),o({currentPriceDiscount:r});const u=a.getCurrentPrice();if(n.textContent=u,a.updatePriceLabel(),a.recalculateFinancials(),updateDiscountButtonText(i),s){const e=c(i.currentPriceDiscount);t(s,e)}}),r.addEventListener("click",function(e){e.preventDefault(),e.stopPropagation(),o({currentPriceDiscount:0});const r=i.originalPrice;if(n.textContent=r,a.updatePriceLabel(),a.recalculateFinancials(),updateDiscountButtonText(i),s){const e=c(i.currentPriceDiscount);t(s,e)}}),s){const t=c(i.currentPriceDiscount);e(s,t),r.classList.add("has-tooltip")}n.style.cursor="pointer",r.style.cursor="pointer"}function setupCapRateClickHandler(n,a,c){if(!n||!a)return;const{state:o,updateState:s}=c;if(!o.isUsingEstimatedCapRate)return;if("true"===n.dataset.handlerAttached)return;n.dataset.handlerAttached="true";const u=n.closest(".metric");if(n.addEventListener("click",function(e){e.preventDefault(),e.stopPropagation();let a=o.currentEstimatedCapRate+1;if(a>20&&(a=5),s({currentEstimatedCapRate:a,capManuallySet:!0}),n.textContent=`${a}%*`,c.recalculateFinancials(),u){const e=r(o.isUsingEstimatedCapRate);e&&t(u,e)}}),a.addEventListener("click",function(e){e.preventDefault(),e.stopPropagation();const a=o.originalEstimatedCapRate||i.DEFAULT_CAP_RATE;if(s({currentEstimatedCapRate:a,capManuallySet:!1,baseNOI:null}),n.textContent=`${a}%*`,c.recalculateFinancials(),u){const e=r(o.isUsingEstimatedCapRate);e&&t(u,e)}}),u){const t=r(o.isUsingEstimatedCapRate);t&&(e(u,t),a.classList.add("has-tooltip"))}n.style.cursor="pointer",a.style.cursor="pointer"}function setupNoiClickHandler(t,e,n){if(!t||!e)return;if("true"===t.dataset.handlerAttached)return;t.dataset.handlerAttached="true";const{state:r,updateState:a}=n;t.addEventListener("click",function(e){if(e.preventDefault(),e.stopPropagation(),"str"!==r.currentPropertyType)return;if(t.querySelector("input"))return;const c=r.cachedStrValue&&Number.isFinite(r.cachedStrValue.value)?String(r.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,.]+/),r=e?parseFloat(e[0].replace(/,/g,"")):NaN;Number.isFinite(r)&&r>0&&a({cachedStrValue:{value:r,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"===r.currentPropertyType&&(a({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,r,c){if(!e||!r)return;if("true"===e.dataset.handlerAttached)return;e.dataset.handlerAttached="true";const{state:o,updateState:s}=c,u=e.closest(".metric");e.addEventListener("click",function(e){e.preventDefault(),e.stopPropagation();let r=o.currentDownPaymentPercent-10,i=o.currentDSCRPercent-10,l=o.currentSellerFiPercent+10;r<0&&(r=60,i=70,l=40),s({currentDownPaymentPercent:r,currentDSCRPercent:i,currentSellerFiPercent:l}),c.updatePercentageLabels(),c.recalculateFinancials(),setTimeout(()=>{const e=document.getElementById("prop-price"),r=document.getElementById("prop-noi");if(e&&r&&u){const c=e.textContent.match(/[\d,]+/),i=r.textContent.match(/[\d,.]+/);if(c&&i){const e=parseFloat(c[0].replace(/,/g,""));let s=parseFloat(i[0].replace(/,/g,""));r.textContent.includes("K")&&(s*=1e3),r.textContent.includes("M")&&(s*=1e6),n(u),setTimeout(()=>{const n=a(e,s,o.currentDownPaymentPercent,o.currentDSCRPercent,o.currentSellerFiPercent,o.currentInterestRateType);t(u,n)},50)}}},100)}),r.addEventListener("click",function(e){e.preventDefault(),e.stopPropagation(),s({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"),r=document.getElementById("prop-noi");if(e&&r&&u){const c=e.textContent.match(/[\d,]+/),i=r.textContent.match(/[\d,.]+/);if(c&&i){const e=parseFloat(c[0].replace(/,/g,""));let s=parseFloat(i[0].replace(/,/g,""));r.textContent.includes("K")&&(s*=1e3),r.textContent.includes("M")&&(s*=1e6),n(u),setTimeout(()=>{const n=a(e,s,o.currentDownPaymentPercent,o.currentDSCRPercent,o.currentSellerFiPercent,o.currentInterestRateType);t(u,n)},50)}}},100)}),u&&r&&r.classList.add("has-tooltip"),e.style.cursor="pointer",r.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\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","setupDownPaymentClickHandler","downElement","downLabelElement","newDownPercent","currentDownPaymentPercent","newDSCRPercent","currentDSCRPercent","newSellerFiPercent","currentSellerFiPercent","updatePercentageLabels","setTimeout","noiElement","priceMatch","match","noiMatch","price","parseFloat","replace","noi","includes","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,WA2CpC,GAzCAP,EAAaJ,iBAAiB,QAAS,SAASC,GAC9CA,EAAEC,iBACFD,EAAEE,kBAEF,IAAIS,EAA4D,GAA9CC,KAAKC,MAAM1B,EAAMK,qBAAuB,IAAW,GACjEmB,EAAc,KAChBA,EAAc,GAGhBb,EAAY,CAAEN,qBAAsBmB,IAEpC,MAAMG,EAAWnB,EAAUS,kBAM3B,GALAD,EAAaZ,YAAcuB,EAC3BnB,EAAUU,mBACVV,EAAUW,wBACVpB,yBAAyBC,GAErBsB,EAAQ,CACV,MAAMM,EAAiBC,EAAyB7B,EAAMK,sBACtDyB,EAAqBR,EAAQM,EAC/B,CACF,GAEAP,EAAkBT,iBAAiB,QAAS,SAASC,GACnDA,EAAEC,iBACFD,EAAEE,kBAEFJ,EAAY,CAAEN,qBAAsB,IAEpC,MAAM0B,EAAa/B,EAAMgC,cAMzB,GALAhB,EAAaZ,YAAc2B,EAC3BvB,EAAUU,mBACVV,EAAUW,wBACVpB,yBAAyBC,GAErBsB,EAAQ,CACV,MAAMM,EAAiBC,EAAyB7B,EAAMK,sBACtDyB,EAAqBR,EAAQM,EAC/B,CACF,GAEIN,EAAQ,CACV,MAAMM,EAAiBC,EAAyB7B,EAAMK,sBACtD4B,EAAcX,EAAQM,GACtBP,EAAkBa,UAAUC,IAAI,cAClC,CAEAnB,EAAaoB,MAAMC,OAAS,UAC5BhB,EAAkBe,MAAMC,OAAS,SACnC,CAEO,SAASC,yBAAyBC,EAAYC,EAAiBhC,GACpE,IAAK+B,IAAeC,EAAiB,OAErC,MAAMxC,MAAEA,EAAKW,YAAEA,GAAgBH,EAC/B,IAAKR,EAAMyC,wBAAyB,OAGpC,GAA2C,SAAvCF,EAAW9B,QAAQC,gBAA4B,OACnD6B,EAAW9B,QAAQC,gBAAkB,OAErC,MAAMY,EAASiB,EAAWhB,QAAQ,WA0ClC,GAxCAgB,EAAW3B,iBAAiB,QAAS,SAASC,GAC5CA,EAAEC,iBACFD,EAAEE,kBAEF,IAAI2B,EAAa1C,EAAM2C,wBAA0B,EAUjD,GATID,EAAa,KACfA,EAAa,GAGf/B,EAAY,CAAEgC,wBAAyBD,EAAYE,gBAAgB,IAEnEL,EAAWnC,YAAc,GAAGsC,MAC5BlC,EAAUW,wBAENG,EAAQ,CACV,MAAMM,EAAiBiB,EAA2B7C,EAAMyC,yBACpDb,GACFE,EAAqBR,EAAQM,EAEjC,CACF,GAEAY,EAAgB5B,iBAAiB,QAAS,SAASC,GACjDA,EAAEC,iBACFD,EAAEE,kBAEF,MAAM+B,EAAkB9C,EAAM+C,0BAA4BC,EAAoBC,iBAM9E,GALAtC,EAAY,CAAEgC,wBAAyBG,EAAiBF,gBAAgB,EAAOM,QAAS,OAExFX,EAAWnC,YAAc,GAAG0C,MAC5BtC,EAAUW,wBAENG,EAAQ,CACV,MAAMM,EAAiBiB,EAA2B7C,EAAMyC,yBACpDb,GACFE,EAAqBR,EAAQM,EAEjC,CACF,GAEIN,EAAQ,CACV,MAAMM,EAAiBiB,EAA2B7C,EAAMyC,yBACpDb,IACFK,EAAcX,EAAQM,GACtBY,EAAgBN,UAAUC,IAAI,eAElC,CAEAI,EAAWH,MAAMC,OAAS,UAC1BG,EAAgBJ,MAAMC,OAAS,SACjC,CAEO,SAASc,6BAA6BC,EAAaC,EAAkB7C,GAC1E,IAAK4C,IAAgBC,EAAkB,OAGvC,GAA4C,SAAxCD,EAAY3C,QAAQC,gBAA4B,OACpD0C,EAAY3C,QAAQC,gBAAkB,OAEtC,MAAMV,MAAEA,EAAKW,YAAEA,GAAgBH,EACzBc,EAAS8B,EAAY7B,QAAQ,WAEnC6B,EAAYxC,iBAAiB,QAAS,SAASC,GAC7CA,EAAEC,iBACFD,EAAEE,kBAEF,IAAIuC,EAAiBtD,EAAMuD,0BAA4B,GACnDC,EAAiBxD,EAAMyD,mBAAqB,GAC5CC,EAAqB1D,EAAM2D,uBAAyB,GAEpDL,EAAiB,IACnBA,EAAiB,GACjBE,EAAiB,GACjBE,EAAqB,IAGvB/C,EAAY,CACV4C,0BAA2BD,EAC3BG,mBAAoBD,EACpBG,uBAAwBD,IAG1BlD,EAAUoD,yBACVpD,EAAUW,wBAEV0C,WAAW,KACT,MAAM7C,EAAed,SAASC,eAAe,cACvC2D,EAAa5D,SAASC,eAAe,YAE3C,GAAIa,GAAgB8C,GAAcxC,EAAQ,CACxC,MAAMyC,EAAa/C,EAAaZ,YAAY4D,MAAM,UAC5CC,EAAWH,EAAW1D,YAAY4D,MAAM,WAE9C,GAAID,GAAcE,EAAU,CAC1B,MAAMC,EAAQC,WAAWJ,EAAW,GAAGK,QAAQ,KAAM,KACrD,IAAIC,EAAMF,WAAWF,EAAS,GAAGG,QAAQ,KAAM,KAE3CN,EAAW1D,YAAYkE,SAAS,OAAMD,GAAO,KAC7CP,EAAW1D,YAAYkE,SAAS,OAAMD,GAAO,KAEjDE,EAAcjD,GACduC,WAAW,KACT,MAAMjC,EAAiB4C,EACrBN,EACAG,EACArE,EAAMuD,0BACNvD,EAAMyD,mBACNzD,EAAM2D,uBACN3D,EAAMyE,yBAER3C,EAAqBR,EAAQM,IAC5B,GACL,CACF,GACC,IACL,GAEAyB,EAAiBzC,iBAAiB,QAAS,SAASC,GAClDA,EAAEC,iBACFD,EAAEE,kBAEFJ,EAAY,CACV4C,0BAAwE,IAA7CP,EAAoB0B,uBAC/CjB,mBAAkE,IAA9CT,EAAoB2B,wBACxChB,uBAA8D,IAAtCX,EAAoB4B,kBAG9CpE,EAAUoD,yBACVpD,EAAUW,wBAEV0C,WAAW,KACT,MAAM7C,EAAed,SAASC,eAAe,cACvC2D,EAAa5D,SAASC,eAAe,YAE3C,GAAIa,GAAgB8C,GAAcxC,EAAQ,CACxC,MAAMyC,EAAa/C,EAAaZ,YAAY4D,MAAM,UAC5CC,EAAWH,EAAW1D,YAAY4D,MAAM,WAE9C,GAAID,GAAcE,EAAU,CAC1B,MAAMC,EAAQC,WAAWJ,EAAW,GAAGK,QAAQ,KAAM,KACrD,IAAIC,EAAMF,WAAWF,EAAS,GAAGG,QAAQ,KAAM,KAE3CN,EAAW1D,YAAYkE,SAAS,OAAMD,GAAO,KAC7CP,EAAW1D,YAAYkE,SAAS,OAAMD,GAAO,KAEjDE,EAAcjD,GACduC,WAAW,KACT,MAAMjC,EAAiB4C,EACrBN,EACAG,EACArE,EAAMuD,0BACNvD,EAAMyD,mBACNzD,EAAM2D,uBACN3D,EAAMyE,yBAER3C,EAAqBR,EAAQM,IAC5B,GACL,CACF,GACC,IACL,GAEIN,GAAU+B,GACZA,EAAiBnB,UAAUC,IAAI,eAGjCiB,EAAYhB,MAAMC,OAAS,UAC3BgB,EAAiBjB,MAAMC,OAAS,SAClC"}
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,2 +1,2 @@
1
- const n=[{value:"multifamily",label:"Multifamily"},{value:"str",label:"STR"},{value:"assisted",label:"Assisted/Co Living"},{value:"business",label:"Business"},{value:"mixed_use",label:"Mixed Use"},{value:"rv_park",label:"RV Park"}];function createPanel(n){const{cssUrls:e=[],defaultPropertyType:s="multifamily",callbacks:a={}}=n||{};console.log("🎨 createPanel() called");const t=document.getElementById("ln-footer");t&&(console.log("🗑️ Removing existing footer"),t.remove()),console.log("📦 Loading CSS files...");const l=e.map(n=>{const e=document.createElement("link");return e.rel="stylesheet",e.href=n,e});let i=0;const c=l.length,onLoad=()=>{i++,console.log(`📄 CSS file loaded (${i}/${c})`),i===c&&(console.log("✨ All CSS loaded, creating footer elements"),createPanelElements(s,a))};l.forEach(n=>{n.onload=onLoad}),console.log("⏰ Setting 100ms fallback timeout"),setTimeout(()=>{console.log("⚠️ Fallback timeout reached, creating footer elements anyway"),createPanelElements(s,a)},100),l.forEach(n=>{document.head.appendChild(n)})}function createPanelElements(e,s){if(document.getElementById("ln-footer"))return;const{updateState:a}=s,t=document.createElement("div");t.id="ln-footer",t.className="ext-footer",t.innerHTML=`\n <div class="footer-container">\n <div class="footer-content">\n <div class="metrics-grid">\n <div class="metric-column">\n <div class="metric">\n <span class="metric-label">Address</span>\n <span id="prop-name" class="metric-value clickable prop-name">Loading...</span>\n </div>\n <div class="metric">\n <span class="metric-label">Lead Status</span>\n <span id="prop-lead-status" class="metric-value prop-lead-status">Loading...</span>\n </div>\n </div>\n\n <div class="metric-column">\n <div class="metric">\n <span class="metric-label">Price</span>\n <span id="prop-price" class="metric-value triangle prop-price">Loading...</span>\n </div>\n <div class="metric">\n <span class="metric-label">Cash Flow</span>\n <span id="prop-cashflow" class="metric-value weight-semibold prop-cashflow">Loading...</span>\n </div>\n </div>\n\n <div class="metric-column">\n <div class="metric">\n <span class="metric-label">Cap Rate</span>\n <span id="prop-cap" class="metric-value prop-cap">Loading...</span>\n </div>\n <div class="metric">\n <span class="metric-label">NOI</span>\n <span id="prop-noi" class="metric-value prop-noi">Loading...</span>\n </div>\n </div>\n\n <div class="metric-column">\n <div class="metric">\n <span class="metric-label">COCR (15%)</span>\n <span id="prop-cocr-15" class="metric-value prop-cocr-15">Loading...</span>\n </div>\n <div class="metric">\n <span class="metric-label">COCR (30%)</span>\n <span id="prop-cocr-30" class="metric-value prop-cocr-30">Loading...</span>\n </div>\n </div>\n\n <div class="metric-column">\n <div class="metric prop-dom-metric">\n <span class="metric-label">DOM</span>\n <span id="prop-dom" class="metric-value triangle prop-dom">Loading...</span>\n </div>\n <div class="metric">\n <span class="metric-label">Equity</span>\n <span id="prop-equity" class="metric-value prop-equity">Loading...</span>\n </div>\n </div>\n\n <div class="metric-column">\n <div class="metric">\n <span class="metric-label">Seller FI (40%)</span>\n <span id="prop-seller-fi" class="metric-value prop-seller-fi">Loading...</span>\n </div>\n <div class="metric">\n <span class="metric-label">Down (60%)</span>\n <span id="prop-down" class="metric-value triangle prop-down">Loading...</span>\n </div>\n </div>\n\n <div class="metric-column">\n <div class="metric">\n <span class="metric-label">DSCR (70%)</span>\n <span id="prop-dscr" class="metric-value prop-dscr">Loading...</span>\n </div>\n <div class="metric">\n <span class="metric-label">SF Payment</span>\n <span id="prop-sf" class="metric-value prop-sf">Loading...</span>\n </div>\n </div>\n\n <div class="metric-column">\n <div class="metric">\n <span class="metric-label">Contact</span>\n <span id="prop-contact" class="metric-value prop-contact">Loading...</span>\n </div>\n <div class="metric">\n <span class="metric-label">Phone</span>\n <span id="prop-phone" class="metric-value prop-contact">Loading...</span>\n </div>\n </div>\n\n <div class="metric-column">\n <div class="metric">\n <span class="metric-label">Net to Buyer</span>\n <span id="prop-net" class="metric-value prop-net">Loading...</span>\n </div>\n <div class="metric">\n <span class="metric-label">Assignment</span>\n <span id="prop-assignment" class="metric-value prop-assignment">Loading...</span>\n </div>\n </div>\n </div>\n </div>\n <div class="footer-controls">\n <div class="footer-controls-col">\n <div class="units-input-row">\n <input type="number" id="ln-units-input" class="units-input" min="1" max="999" value="4">\n <span class="units-inline-label">units</span>\n </div>\n <button class="btn-discount" id="ln-discount-btn">85% of Asking</button>\n </div>\n <div class="footer-controls-col">\n <select class="dropdown" id="ln-property-type">\n ${function(e){return n.map(({value:n,label:s})=>`<option value="${n}"${n===e?" selected":""}>${s}</option>`).join("\n ")}(e)}\n </select>\n <select class="dropdown" id="ln-interest-rate-type">\n <option value="dscr_residential" selected>DSCR Res (8%)</option>\n <option value="dscr_commercial">DSCR Com (10%)</option>\n <option value="commercial">Commercial (10%)</option>\n <option value="mixed_use">Mixed Use (10%)</option>\n <option value="rv_park">RV Park (11%)</option>\n </select>\n <button class="btn-primary" id="ln-export-btn" title="Open dashboard with property data">\n <svg class="icon" viewBox="0 0 24 24">\n <path d="M14,2H6A2,2 0 0,0 4,4V20A2,2 0 0,0 6,22H18A2,2 0 0,0 20,20V8L14,2M18,20H6V4H13V9H18V20Z" />\n </svg>\n Dashboard\n </button>\n </div>\n </div>\n </div>\n `;try{document.body?document.body.appendChild(t):document.documentElement&&document.documentElement.appendChild(t)}catch(n){}const l=document.getElementById("ln-export-btn");l&&l.addEventListener("click",()=>{s.onExportClick?.()});const i=document.getElementById("ln-property-type");i&&i.addEventListener("change",()=>{s.onPropertyTypeChange?.(i.value)});const c=document.getElementById("ln-interest-rate-type");c&&c.addEventListener("change",()=>{a({currentInterestRateType:c.value}),s.onInterestRateTypeChange?.(c.value)});const o=document.getElementById("ln-units-input");return o&&o.addEventListener("change",()=>{const n=parseInt(o.value)||4;a({numberOfUnits:n});const e=document.getElementById("ln-interest-rate-type");e&&(n>11&&"dscr_commercial"!==e.value?(e.value="dscr_commercial",a({currentInterestRateType:"dscr_commercial"}),s.onInterestRateTypeChange?.("dscr_commercial")):n<=11&&"dscr_commercial"===e.value&&(e.value="dscr_residential",a({currentInterestRateType:"dscr_residential"}),s.onInterestRateTypeChange?.("dscr_residential")))}),t}export{createPanel};
1
+ const n=[{value:"multifamily",label:"Multifamily"},{value:"str",label:"STR"},{value:"assisted",label:"Assisted/Co Living"},{value:"business",label:"Business"},{value:"mixed_use",label:"Mixed Use"},{value:"rv_park",label:"RV Park"}];function createPanel(n){const{cssUrls:e=[],defaultPropertyType:a="multifamily",callbacks:s={}}=n||{};console.log("🎨 createPanel() called");const t=document.getElementById("ln-footer");t&&(console.log("🗑️ Removing existing footer"),t.remove()),console.log("📦 Loading CSS files...");const l=e.map(n=>{const e=document.createElement("link");return e.rel="stylesheet",e.href=n,e});let i=0;const c=l.length,onLoad=()=>{i++,console.log(`📄 CSS file loaded (${i}/${c})`),i===c&&(console.log("✨ All CSS loaded, creating footer elements"),createPanelElements(a,s))};l.forEach(n=>{n.onload=onLoad}),console.log("⏰ Setting 100ms fallback timeout"),setTimeout(()=>{console.log("⚠️ Fallback timeout reached, creating footer elements anyway"),createPanelElements(a,s)},100),l.forEach(n=>{document.head.appendChild(n)})}function createPanelElements(e,a){if(document.getElementById("ln-footer"))return;const{updateState:s}=a,t=document.createElement("div");t.id="ln-footer",t.className="ext-footer",t.innerHTML=`\n <div class="footer-container">\n <div class="footer-content">\n <div class="metrics-grid">\n <div class="metric-column">\n <div class="metric">\n <span class="metric-label">Address</span>\n <span id="prop-name" class="metric-value clickable prop-name">Loading...</span>\n </div>\n <div class="metric">\n <span class="metric-label">Lead Status</span>\n <span id="prop-lead-status" class="metric-value prop-lead-status">Loading...</span>\n </div>\n </div>\n\n <div class="metric-column">\n <div class="metric">\n <span class="metric-label">Price</span>\n <span id="prop-price" class="metric-value triangle prop-price">Loading...</span>\n </div>\n <div class="metric">\n <span class="metric-label">Cash Flow</span>\n <span id="prop-cashflow" class="metric-value weight-semibold prop-cashflow">Loading...</span>\n </div>\n </div>\n\n <div class="metric-column">\n <div class="metric">\n <span class="metric-label">Cap Rate</span>\n <span id="prop-cap" class="metric-value prop-cap">Loading...</span>\n </div>\n <div class="metric">\n <span class="metric-label">NOI</span>\n <span id="prop-noi" class="metric-value prop-noi">Loading...</span>\n <a id="prop-noi-awning" class="noi-awning-link" target="_blank" rel="noopener" title="Open Awning's calculator and copy this address to the clipboard" style="display:none;cursor:pointer;font-size:11px;font-weight:600;color:#200955;text-decoration:none;margin-top:2px;">↗ Awning</a>\n </div>\n </div>\n\n <div class="metric-column">\n <div class="metric">\n <span class="metric-label">COCR (15%)</span>\n <span id="prop-cocr-15" class="metric-value prop-cocr-15">Loading...</span>\n </div>\n <div class="metric">\n <span class="metric-label">COCR (30%)</span>\n <span id="prop-cocr-30" class="metric-value prop-cocr-30">Loading...</span>\n </div>\n </div>\n\n <div class="metric-column">\n <div class="metric prop-dom-metric">\n <span class="metric-label">DOM</span>\n <span id="prop-dom" class="metric-value triangle prop-dom">Loading...</span>\n </div>\n <div class="metric">\n <span class="metric-label">Equity</span>\n <span id="prop-equity" class="metric-value prop-equity">Loading...</span>\n </div>\n </div>\n\n <div class="metric-column">\n <div class="metric">\n <span class="metric-label">Seller FI (40%)</span>\n <span id="prop-seller-fi" class="metric-value prop-seller-fi">Loading...</span>\n </div>\n <div class="metric">\n <span class="metric-label">Down (60%)</span>\n <span id="prop-down" class="metric-value triangle prop-down">Loading...</span>\n </div>\n </div>\n\n <div class="metric-column">\n <div class="metric">\n <span class="metric-label">DSCR (70%)</span>\n <span id="prop-dscr" class="metric-value prop-dscr">Loading...</span>\n </div>\n <div class="metric">\n <span class="metric-label">SF Payment</span>\n <span id="prop-sf" class="metric-value prop-sf">Loading...</span>\n </div>\n </div>\n\n <div class="metric-column">\n <div class="metric">\n <span class="metric-label">Contact</span>\n <span id="prop-contact" class="metric-value prop-contact">Loading...</span>\n </div>\n <div class="metric">\n <span class="metric-label">Phone</span>\n <span id="prop-phone" class="metric-value prop-contact">Loading...</span>\n </div>\n </div>\n\n <div class="metric-column">\n <div class="metric">\n <span class="metric-label">Net to Buyer</span>\n <span id="prop-net" class="metric-value prop-net">Loading...</span>\n </div>\n <div class="metric">\n <span class="metric-label">Assignment</span>\n <span id="prop-assignment" class="metric-value prop-assignment">Loading...</span>\n </div>\n </div>\n </div>\n </div>\n <div class="footer-controls">\n <div class="footer-controls-col">\n <div class="units-input-row">\n <input type="number" id="ln-units-input" class="units-input" min="1" max="999" value="4">\n <span class="units-inline-label">units</span>\n </div>\n <button class="btn-discount" id="ln-discount-btn">85% of Asking</button>\n </div>\n <div class="footer-controls-col">\n <select class="dropdown" id="ln-property-type">\n ${function(e){return n.map(({value:n,label:a})=>`<option value="${n}"${n===e?" selected":""}>${a}</option>`).join("\n ")}(e)}\n </select>\n <select class="dropdown" id="ln-interest-rate-type">\n <option value="dscr_residential" selected>DSCR Res (8%)</option>\n <option value="dscr_commercial">DSCR Com (10%)</option>\n <option value="commercial">Commercial (10%)</option>\n <option value="mixed_use">Mixed Use (10%)</option>\n <option value="rv_park">RV Park (11%)</option>\n </select>\n <button class="btn-primary" id="ln-export-btn" title="Open dashboard with property data">\n <svg class="icon" viewBox="0 0 24 24">\n <path d="M14,2H6A2,2 0 0,0 4,4V20A2,2 0 0,0 6,22H18A2,2 0 0,0 20,20V8L14,2M18,20H6V4H13V9H18V20Z" />\n </svg>\n Dashboard\n </button>\n </div>\n </div>\n </div>\n `;try{document.body?document.body.appendChild(t):document.documentElement&&document.documentElement.appendChild(t)}catch(n){}const l=document.getElementById("ln-export-btn");l&&l.addEventListener("click",()=>{a.onExportClick?.()});const i=document.getElementById("ln-property-type");i&&i.addEventListener("change",()=>{a.onPropertyTypeChange?.(i.value)});const c=document.getElementById("ln-interest-rate-type");c&&c.addEventListener("change",()=>{s({currentInterestRateType:c.value}),a.onInterestRateTypeChange?.(c.value)});const o=document.getElementById("ln-units-input");return o&&o.addEventListener("change",()=>{const n=parseInt(o.value)||4;s({numberOfUnits:n});const e=document.getElementById("ln-interest-rate-type");e&&(n>11&&"dscr_commercial"!==e.value?(e.value="dscr_commercial",s({currentInterestRateType:"dscr_commercial"}),a.onInterestRateTypeChange?.("dscr_commercial")):n<=11&&"dscr_commercial"===e.value&&(e.value="dscr_residential",s({currentInterestRateType:"dscr_residential"}),a.onInterestRateTypeChange?.("dscr_residential")))}),t}export{createPanel};
2
2
  //# sourceMappingURL=createPanel.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"createPanel.js","sources":["../../../src/browser/widget/createPanel.js"],"sourcesContent":["// Shared analysis-panel (footer) builder for the property analyzers.\r\n//\r\n// Extracted verbatim from each analyzer's dom-utils.js createFooter/createFooterElements.\r\n// Per-platform values are injected so this module has no Chrome or global-state dependency:\r\n// - cssUrls: stylesheet hrefs (callers resolve chrome.runtime.getURL themselves)\r\n// - defaultPropertyType: which property-type <option> is pre-selected (loopnet \"multifamily\", zillow \"str\")\r\n// - callbacks.state / callbacks.updateState: the per-platform state singleton\r\n// - callbacks.onExportClick / onPropertyTypeChange / onInterestRateTypeChange: optional notifications\r\n// (today these are CustomEvent dispatches; callers that need them pass a handler, others omit)\r\n\r\nconst PROPERTY_TYPE_OPTIONS = [\r\n { value: \"multifamily\", label: \"Multifamily\" },\r\n { value: \"str\", label: \"STR\" },\r\n { value: \"assisted\", label: \"Assisted/Co Living\" },\r\n { value: \"business\", label: \"Business\" },\r\n { value: \"mixed_use\", label: \"Mixed Use\" },\r\n { value: \"rv_park\", label: \"RV Park\" }\r\n];\r\n\r\nfunction renderPropertyTypeOptions(defaultPropertyType) {\r\n return PROPERTY_TYPE_OPTIONS.map(({ value, label }) => {\r\n const selected = value === defaultPropertyType ? \" selected\" : \"\";\r\n return `<option value=\"${value}\"${selected}>${label}</option>`;\r\n }).join(\"\\n \");\r\n}\r\n\r\nexport function createPanel(config) {\r\n const { cssUrls = [], defaultPropertyType = \"multifamily\", callbacks = {} } = config || {};\r\n\r\n console.log(\"🎨 createPanel() called\");\r\n\r\n const existing = document.getElementById(\"ln-footer\");\r\n if (existing) {\r\n console.log(\"🗑️ Removing existing footer\");\r\n existing.remove();\r\n }\r\n\r\n console.log(\"📦 Loading CSS files...\");\r\n\r\n const links = cssUrls.map((href) => {\r\n const link = document.createElement(\"link\");\r\n link.rel = \"stylesheet\";\r\n link.href = href;\r\n return link;\r\n });\r\n\r\n // Wait for all stylesheets to load before creating footer\r\n let loadedCount = 0;\r\n const total = links.length;\r\n const onLoad = () => {\r\n loadedCount++;\r\n console.log(`📄 CSS file loaded (${loadedCount}/${total})`);\r\n if (loadedCount === total) {\r\n console.log(\"✨ All CSS loaded, creating footer elements\");\r\n createPanelElements(defaultPropertyType, callbacks);\r\n }\r\n };\r\n\r\n links.forEach((link) => { link.onload = onLoad; });\r\n\r\n console.log(\"⏰ Setting 100ms fallback timeout\");\r\n // Fallback - create footer after timeout even if shared CSS doesn't load\r\n setTimeout(() => {\r\n console.log(\"⚠️ Fallback timeout reached, creating footer elements anyway\");\r\n createPanelElements(defaultPropertyType, callbacks);\r\n }, 100);\r\n\r\n links.forEach((link) => { document.head.appendChild(link); });\r\n}\r\n\r\nfunction createPanelElements(defaultPropertyType, callbacks) {\r\n // Prevent duplicate creation\r\n if (document.getElementById(\"ln-footer\")) return;\r\n\r\n const { updateState } = callbacks;\r\n\r\n const footer = document.createElement(\"div\");\r\n footer.id = \"ln-footer\";\r\n footer.className = \"ext-footer\";\r\n\r\n footer.innerHTML = `\r\n <div class=\"footer-container\">\r\n <div class=\"footer-content\">\r\n <div class=\"metrics-grid\">\r\n <div class=\"metric-column\">\r\n <div class=\"metric\">\r\n <span class=\"metric-label\">Address</span>\r\n <span id=\"prop-name\" class=\"metric-value clickable prop-name\">Loading...</span>\r\n </div>\r\n <div class=\"metric\">\r\n <span class=\"metric-label\">Lead Status</span>\r\n <span id=\"prop-lead-status\" class=\"metric-value prop-lead-status\">Loading...</span>\r\n </div>\r\n </div>\r\n\r\n <div class=\"metric-column\">\r\n <div class=\"metric\">\r\n <span class=\"metric-label\">Price</span>\r\n <span id=\"prop-price\" class=\"metric-value triangle prop-price\">Loading...</span>\r\n </div>\r\n <div class=\"metric\">\r\n <span class=\"metric-label\">Cash Flow</span>\r\n <span id=\"prop-cashflow\" class=\"metric-value weight-semibold prop-cashflow\">Loading...</span>\r\n </div>\r\n </div>\r\n\r\n <div class=\"metric-column\">\r\n <div class=\"metric\">\r\n <span class=\"metric-label\">Cap Rate</span>\r\n <span id=\"prop-cap\" class=\"metric-value prop-cap\">Loading...</span>\r\n </div>\r\n <div class=\"metric\">\r\n <span class=\"metric-label\">NOI</span>\r\n <span id=\"prop-noi\" class=\"metric-value prop-noi\">Loading...</span>\r\n </div>\r\n </div>\r\n\r\n <div class=\"metric-column\">\r\n <div class=\"metric\">\r\n <span class=\"metric-label\">COCR (15%)</span>\r\n <span id=\"prop-cocr-15\" class=\"metric-value prop-cocr-15\">Loading...</span>\r\n </div>\r\n <div class=\"metric\">\r\n <span class=\"metric-label\">COCR (30%)</span>\r\n <span id=\"prop-cocr-30\" class=\"metric-value prop-cocr-30\">Loading...</span>\r\n </div>\r\n </div>\r\n\r\n <div class=\"metric-column\">\r\n <div class=\"metric prop-dom-metric\">\r\n <span class=\"metric-label\">DOM</span>\r\n <span id=\"prop-dom\" class=\"metric-value triangle prop-dom\">Loading...</span>\r\n </div>\r\n <div class=\"metric\">\r\n <span class=\"metric-label\">Equity</span>\r\n <span id=\"prop-equity\" class=\"metric-value prop-equity\">Loading...</span>\r\n </div>\r\n </div>\r\n\r\n <div class=\"metric-column\">\r\n <div class=\"metric\">\r\n <span class=\"metric-label\">Seller FI (40%)</span>\r\n <span id=\"prop-seller-fi\" class=\"metric-value prop-seller-fi\">Loading...</span>\r\n </div>\r\n <div class=\"metric\">\r\n <span class=\"metric-label\">Down (60%)</span>\r\n <span id=\"prop-down\" class=\"metric-value triangle prop-down\">Loading...</span>\r\n </div>\r\n </div>\r\n\r\n <div class=\"metric-column\">\r\n <div class=\"metric\">\r\n <span class=\"metric-label\">DSCR (70%)</span>\r\n <span id=\"prop-dscr\" class=\"metric-value prop-dscr\">Loading...</span>\r\n </div>\r\n <div class=\"metric\">\r\n <span class=\"metric-label\">SF Payment</span>\r\n <span id=\"prop-sf\" class=\"metric-value prop-sf\">Loading...</span>\r\n </div>\r\n </div>\r\n\r\n <div class=\"metric-column\">\r\n <div class=\"metric\">\r\n <span class=\"metric-label\">Contact</span>\r\n <span id=\"prop-contact\" class=\"metric-value prop-contact\">Loading...</span>\r\n </div>\r\n <div class=\"metric\">\r\n <span class=\"metric-label\">Phone</span>\r\n <span id=\"prop-phone\" class=\"metric-value prop-contact\">Loading...</span>\r\n </div>\r\n </div>\r\n\r\n <div class=\"metric-column\">\r\n <div class=\"metric\">\r\n <span class=\"metric-label\">Net to Buyer</span>\r\n <span id=\"prop-net\" class=\"metric-value prop-net\">Loading...</span>\r\n </div>\r\n <div class=\"metric\">\r\n <span class=\"metric-label\">Assignment</span>\r\n <span id=\"prop-assignment\" class=\"metric-value prop-assignment\">Loading...</span>\r\n </div>\r\n </div>\r\n </div>\r\n </div>\r\n <div class=\"footer-controls\">\r\n <div class=\"footer-controls-col\">\r\n <div class=\"units-input-row\">\r\n <input type=\"number\" id=\"ln-units-input\" class=\"units-input\" min=\"1\" max=\"999\" value=\"4\">\r\n <span class=\"units-inline-label\">units</span>\r\n </div>\r\n <button class=\"btn-discount\" id=\"ln-discount-btn\">85% of Asking</button>\r\n </div>\r\n <div class=\"footer-controls-col\">\r\n <select class=\"dropdown\" id=\"ln-property-type\">\r\n ${renderPropertyTypeOptions(defaultPropertyType)}\r\n </select>\r\n <select class=\"dropdown\" id=\"ln-interest-rate-type\">\r\n <option value=\"dscr_residential\" selected>DSCR Res (8%)</option>\r\n <option value=\"dscr_commercial\">DSCR Com (10%)</option>\r\n <option value=\"commercial\">Commercial (10%)</option>\r\n <option value=\"mixed_use\">Mixed Use (10%)</option>\r\n <option value=\"rv_park\">RV Park (11%)</option>\r\n </select>\r\n <button class=\"btn-primary\" id=\"ln-export-btn\" title=\"Open dashboard with property data\">\r\n <svg class=\"icon\" viewBox=\"0 0 24 24\">\r\n <path d=\"M14,2H6A2,2 0 0,0 4,4V20A2,2 0 0,0 6,22H18A2,2 0 0,0 20,20V8L14,2M18,20H6V4H13V9H18V20Z\" />\r\n </svg>\r\n Dashboard\r\n </button>\r\n </div>\r\n </div>\r\n </div>\r\n `;\r\n\r\n try {\r\n if (document.body) {\r\n document.body.appendChild(footer);\r\n } else if (document.documentElement) {\r\n document.documentElement.appendChild(footer);\r\n }\r\n } catch (error) {\r\n // Silent fail\r\n }\r\n\r\n const exportBtn = document.getElementById(\"ln-export-btn\");\r\n if (exportBtn) {\r\n exportBtn.addEventListener(\"click\", () => {\r\n callbacks.onExportClick?.();\r\n });\r\n }\r\n\r\n const propertyTypeDropdown = document.getElementById(\"ln-property-type\");\r\n if (propertyTypeDropdown) {\r\n propertyTypeDropdown.addEventListener(\"change\", () => {\r\n callbacks.onPropertyTypeChange?.(propertyTypeDropdown.value);\r\n });\r\n }\r\n\r\n const interestRateTypeDropdown = document.getElementById(\"ln-interest-rate-type\");\r\n if (interestRateTypeDropdown) {\r\n interestRateTypeDropdown.addEventListener(\"change\", () => {\r\n updateState({ currentInterestRateType: interestRateTypeDropdown.value });\r\n callbacks.onInterestRateTypeChange?.(interestRateTypeDropdown.value);\r\n });\r\n }\r\n\r\n const unitsInput = document.getElementById(\"ln-units-input\");\r\n if (unitsInput) {\r\n unitsInput.addEventListener(\"change\", () => {\r\n const value = parseInt(unitsInput.value) || 4;\r\n updateState({ numberOfUnits: value });\r\n\r\n const irDropdown = document.getElementById(\"ln-interest-rate-type\");\r\n if (irDropdown) {\r\n if (value > 11 && irDropdown.value !== \"dscr_commercial\") {\r\n irDropdown.value = \"dscr_commercial\";\r\n updateState({ currentInterestRateType: \"dscr_commercial\" });\r\n callbacks.onInterestRateTypeChange?.(\"dscr_commercial\");\r\n } else if (value <= 11 && irDropdown.value === \"dscr_commercial\") {\r\n irDropdown.value = \"dscr_residential\";\r\n updateState({ currentInterestRateType: \"dscr_residential\" });\r\n callbacks.onInterestRateTypeChange?.(\"dscr_residential\");\r\n }\r\n }\r\n });\r\n }\r\n\r\n return footer;\r\n}\r\n"],"names":["PROPERTY_TYPE_OPTIONS","value","label","createPanel","config","cssUrls","defaultPropertyType","callbacks","console","log","existing","document","getElementById","remove","links","map","href","link","createElement","rel","loadedCount","total","length","onLoad","createPanelElements","forEach","onload","setTimeout","head","appendChild","updateState","footer","id","className","innerHTML","join","renderPropertyTypeOptions","body","documentElement","error","exportBtn","addEventListener","onExportClick","propertyTypeDropdown","onPropertyTypeChange","interestRateTypeDropdown","currentInterestRateType","onInterestRateTypeChange","unitsInput","parseInt","numberOfUnits","irDropdown"],"mappings":"AAUA,MAAMA,EAAwB,CAC5B,CAAEC,MAAO,cAAeC,MAAO,eAC/B,CAAED,MAAO,MAAOC,MAAO,OACvB,CAAED,MAAO,WAAYC,MAAO,sBAC5B,CAAED,MAAO,WAAYC,MAAO,YAC5B,CAAED,MAAO,YAAaC,MAAO,aAC7B,CAAED,MAAO,UAAWC,MAAO,YAUtB,SAASC,YAAYC,GAC1B,MAAMC,QAAEA,EAAU,GAAEC,oBAAEA,EAAsB,cAAaC,UAAEA,EAAY,CAAA,GAAOH,GAAU,GAExFI,QAAQC,IAAI,2BAEZ,MAAMC,EAAWC,SAASC,eAAe,aACrCF,IACFF,QAAQC,IAAI,gCACZC,EAASG,UAGXL,QAAQC,IAAI,2BAEZ,MAAMK,EAAQT,EAAQU,IAAKC,IACzB,MAAMC,EAAON,SAASO,cAAc,QAGpC,OAFAD,EAAKE,IAAM,aACXF,EAAKD,KAAOA,EACLC,IAIT,IAAIG,EAAc,EAClB,MAAMC,EAAQP,EAAMQ,OACdC,OAAS,KACbH,IACAZ,QAAQC,IAAI,uBAAuBW,KAAeC,MAC9CD,IAAgBC,IAClBb,QAAQC,IAAI,8CACZe,oBAAoBlB,EAAqBC,KAI7CO,EAAMW,QAASR,IAAWA,EAAKS,OAASH,SAExCf,QAAQC,IAAI,oCAEZkB,WAAW,KACTnB,QAAQC,IAAI,gEACZe,oBAAoBlB,EAAqBC,IACxC,KAEHO,EAAMW,QAASR,IAAWN,SAASiB,KAAKC,YAAYZ,IACtD,CAEA,SAASO,oBAAoBlB,EAAqBC,GAEhD,GAAII,SAASC,eAAe,aAAc,OAE1C,MAAMkB,YAAEA,GAAgBvB,EAElBwB,EAASpB,SAASO,cAAc,OACtCa,EAAOC,GAAK,YACZD,EAAOE,UAAY,aAEnBF,EAAOG,UAAY,qwJA7DrB,SAAmC5B,GACjC,OAAON,EAAsBe,IAAI,EAAGd,QAAOC,WAElC,kBAAkBD,KADRA,IAAUK,EAAsB,YAAc,MACjBJ,cAC7CiC,KAAK,iBACV,CA0KcC,CAA0B9B,2zBAoBtC,IACMK,SAAS0B,KACX1B,SAAS0B,KAAKR,YAAYE,GACjBpB,SAAS2B,iBAClB3B,SAAS2B,gBAAgBT,YAAYE,EAEzC,CAAE,MAAOQ,GAET,CAEA,MAAMC,EAAY7B,SAASC,eAAe,iBACtC4B,GACFA,EAAUC,iBAAiB,QAAS,KAClClC,EAAUmC,oBAId,MAAMC,EAAuBhC,SAASC,eAAe,oBACjD+B,GACFA,EAAqBF,iBAAiB,SAAU,KAC9ClC,EAAUqC,uBAAuBD,EAAqB1C,SAI1D,MAAM4C,EAA2BlC,SAASC,eAAe,yBACrDiC,GACFA,EAAyBJ,iBAAiB,SAAU,KAClDX,EAAY,CAAEgB,wBAAyBD,EAAyB5C,QAChEM,EAAUwC,2BAA2BF,EAAyB5C,SAIlE,MAAM+C,EAAarC,SAASC,eAAe,kBAqB3C,OApBIoC,GACFA,EAAWP,iBAAiB,SAAU,KACpC,MAAMxC,EAAQgD,SAASD,EAAW/C,QAAU,EAC5C6B,EAAY,CAAEoB,cAAejD,IAE7B,MAAMkD,EAAaxC,SAASC,eAAe,yBACvCuC,IACElD,EAAQ,IAA2B,oBAArBkD,EAAWlD,OAC3BkD,EAAWlD,MAAQ,kBACnB6B,EAAY,CAAEgB,wBAAyB,oBACvCvC,EAAUwC,2BAA2B,oBAC5B9C,GAAS,IAA2B,oBAArBkD,EAAWlD,QACnCkD,EAAWlD,MAAQ,mBACnB6B,EAAY,CAAEgB,wBAAyB,qBACvCvC,EAAUwC,2BAA2B,wBAMtChB,CACT"}
1
+ {"version":3,"file":"createPanel.js","sources":["../../../src/browser/widget/createPanel.js"],"sourcesContent":["// Shared analysis-panel (footer) builder for the property analyzers.\r\n//\r\n// Extracted verbatim from each analyzer's dom-utils.js createFooter/createFooterElements.\r\n// Per-platform values are injected so this module has no Chrome or global-state dependency:\r\n// - cssUrls: stylesheet hrefs (callers resolve chrome.runtime.getURL themselves)\r\n// - defaultPropertyType: which property-type <option> is pre-selected (loopnet \"multifamily\", zillow \"str\")\r\n// - callbacks.state / callbacks.updateState: the per-platform state singleton\r\n// - callbacks.onExportClick / onPropertyTypeChange / onInterestRateTypeChange: optional notifications\r\n// (today these are CustomEvent dispatches; callers that need them pass a handler, others omit)\r\n\r\nconst PROPERTY_TYPE_OPTIONS = [\r\n { value: \"multifamily\", label: \"Multifamily\" },\r\n { value: \"str\", label: \"STR\" },\r\n { value: \"assisted\", label: \"Assisted/Co Living\" },\r\n { value: \"business\", label: \"Business\" },\r\n { value: \"mixed_use\", label: \"Mixed Use\" },\r\n { value: \"rv_park\", label: \"RV Park\" }\r\n];\r\n\r\nfunction renderPropertyTypeOptions(defaultPropertyType) {\r\n return PROPERTY_TYPE_OPTIONS.map(({ value, label }) => {\r\n const selected = value === defaultPropertyType ? \" selected\" : \"\";\r\n return `<option value=\"${value}\"${selected}>${label}</option>`;\r\n }).join(\"\\n \");\r\n}\r\n\r\nexport function createPanel(config) {\r\n const { cssUrls = [], defaultPropertyType = \"multifamily\", callbacks = {} } = config || {};\r\n\r\n console.log(\"🎨 createPanel() called\");\r\n\r\n const existing = document.getElementById(\"ln-footer\");\r\n if (existing) {\r\n console.log(\"🗑️ Removing existing footer\");\r\n existing.remove();\r\n }\r\n\r\n console.log(\"📦 Loading CSS files...\");\r\n\r\n const links = cssUrls.map((href) => {\r\n const link = document.createElement(\"link\");\r\n link.rel = \"stylesheet\";\r\n link.href = href;\r\n return link;\r\n });\r\n\r\n // Wait for all stylesheets to load before creating footer\r\n let loadedCount = 0;\r\n const total = links.length;\r\n const onLoad = () => {\r\n loadedCount++;\r\n console.log(`📄 CSS file loaded (${loadedCount}/${total})`);\r\n if (loadedCount === total) {\r\n console.log(\"✨ All CSS loaded, creating footer elements\");\r\n createPanelElements(defaultPropertyType, callbacks);\r\n }\r\n };\r\n\r\n links.forEach((link) => { link.onload = onLoad; });\r\n\r\n console.log(\"⏰ Setting 100ms fallback timeout\");\r\n // Fallback - create footer after timeout even if shared CSS doesn't load\r\n setTimeout(() => {\r\n console.log(\"⚠️ Fallback timeout reached, creating footer elements anyway\");\r\n createPanelElements(defaultPropertyType, callbacks);\r\n }, 100);\r\n\r\n links.forEach((link) => { document.head.appendChild(link); });\r\n}\r\n\r\nfunction createPanelElements(defaultPropertyType, callbacks) {\r\n // Prevent duplicate creation\r\n if (document.getElementById(\"ln-footer\")) return;\r\n\r\n const { updateState } = callbacks;\r\n\r\n const footer = document.createElement(\"div\");\r\n footer.id = \"ln-footer\";\r\n footer.className = \"ext-footer\";\r\n\r\n footer.innerHTML = `\r\n <div class=\"footer-container\">\r\n <div class=\"footer-content\">\r\n <div class=\"metrics-grid\">\r\n <div class=\"metric-column\">\r\n <div class=\"metric\">\r\n <span class=\"metric-label\">Address</span>\r\n <span id=\"prop-name\" class=\"metric-value clickable prop-name\">Loading...</span>\r\n </div>\r\n <div class=\"metric\">\r\n <span class=\"metric-label\">Lead Status</span>\r\n <span id=\"prop-lead-status\" class=\"metric-value prop-lead-status\">Loading...</span>\r\n </div>\r\n </div>\r\n\r\n <div class=\"metric-column\">\r\n <div class=\"metric\">\r\n <span class=\"metric-label\">Price</span>\r\n <span id=\"prop-price\" class=\"metric-value triangle prop-price\">Loading...</span>\r\n </div>\r\n <div class=\"metric\">\r\n <span class=\"metric-label\">Cash Flow</span>\r\n <span id=\"prop-cashflow\" class=\"metric-value weight-semibold prop-cashflow\">Loading...</span>\r\n </div>\r\n </div>\r\n\r\n <div class=\"metric-column\">\r\n <div class=\"metric\">\r\n <span class=\"metric-label\">Cap Rate</span>\r\n <span id=\"prop-cap\" class=\"metric-value prop-cap\">Loading...</span>\r\n </div>\r\n <div class=\"metric\">\r\n <span class=\"metric-label\">NOI</span>\r\n <span id=\"prop-noi\" class=\"metric-value prop-noi\">Loading...</span>\r\n <a id=\"prop-noi-awning\" class=\"noi-awning-link\" target=\"_blank\" rel=\"noopener\" title=\"Open Awning's calculator and copy this address to the clipboard\" style=\"display:none;cursor:pointer;font-size:11px;font-weight:600;color:#200955;text-decoration:none;margin-top:2px;\">↗ Awning</a>\r\n </div>\r\n </div>\r\n\r\n <div class=\"metric-column\">\r\n <div class=\"metric\">\r\n <span class=\"metric-label\">COCR (15%)</span>\r\n <span id=\"prop-cocr-15\" class=\"metric-value prop-cocr-15\">Loading...</span>\r\n </div>\r\n <div class=\"metric\">\r\n <span class=\"metric-label\">COCR (30%)</span>\r\n <span id=\"prop-cocr-30\" class=\"metric-value prop-cocr-30\">Loading...</span>\r\n </div>\r\n </div>\r\n\r\n <div class=\"metric-column\">\r\n <div class=\"metric prop-dom-metric\">\r\n <span class=\"metric-label\">DOM</span>\r\n <span id=\"prop-dom\" class=\"metric-value triangle prop-dom\">Loading...</span>\r\n </div>\r\n <div class=\"metric\">\r\n <span class=\"metric-label\">Equity</span>\r\n <span id=\"prop-equity\" class=\"metric-value prop-equity\">Loading...</span>\r\n </div>\r\n </div>\r\n\r\n <div class=\"metric-column\">\r\n <div class=\"metric\">\r\n <span class=\"metric-label\">Seller FI (40%)</span>\r\n <span id=\"prop-seller-fi\" class=\"metric-value prop-seller-fi\">Loading...</span>\r\n </div>\r\n <div class=\"metric\">\r\n <span class=\"metric-label\">Down (60%)</span>\r\n <span id=\"prop-down\" class=\"metric-value triangle prop-down\">Loading...</span>\r\n </div>\r\n </div>\r\n\r\n <div class=\"metric-column\">\r\n <div class=\"metric\">\r\n <span class=\"metric-label\">DSCR (70%)</span>\r\n <span id=\"prop-dscr\" class=\"metric-value prop-dscr\">Loading...</span>\r\n </div>\r\n <div class=\"metric\">\r\n <span class=\"metric-label\">SF Payment</span>\r\n <span id=\"prop-sf\" class=\"metric-value prop-sf\">Loading...</span>\r\n </div>\r\n </div>\r\n\r\n <div class=\"metric-column\">\r\n <div class=\"metric\">\r\n <span class=\"metric-label\">Contact</span>\r\n <span id=\"prop-contact\" class=\"metric-value prop-contact\">Loading...</span>\r\n </div>\r\n <div class=\"metric\">\r\n <span class=\"metric-label\">Phone</span>\r\n <span id=\"prop-phone\" class=\"metric-value prop-contact\">Loading...</span>\r\n </div>\r\n </div>\r\n\r\n <div class=\"metric-column\">\r\n <div class=\"metric\">\r\n <span class=\"metric-label\">Net to Buyer</span>\r\n <span id=\"prop-net\" class=\"metric-value prop-net\">Loading...</span>\r\n </div>\r\n <div class=\"metric\">\r\n <span class=\"metric-label\">Assignment</span>\r\n <span id=\"prop-assignment\" class=\"metric-value prop-assignment\">Loading...</span>\r\n </div>\r\n </div>\r\n </div>\r\n </div>\r\n <div class=\"footer-controls\">\r\n <div class=\"footer-controls-col\">\r\n <div class=\"units-input-row\">\r\n <input type=\"number\" id=\"ln-units-input\" class=\"units-input\" min=\"1\" max=\"999\" value=\"4\">\r\n <span class=\"units-inline-label\">units</span>\r\n </div>\r\n <button class=\"btn-discount\" id=\"ln-discount-btn\">85% of Asking</button>\r\n </div>\r\n <div class=\"footer-controls-col\">\r\n <select class=\"dropdown\" id=\"ln-property-type\">\r\n ${renderPropertyTypeOptions(defaultPropertyType)}\r\n </select>\r\n <select class=\"dropdown\" id=\"ln-interest-rate-type\">\r\n <option value=\"dscr_residential\" selected>DSCR Res (8%)</option>\r\n <option value=\"dscr_commercial\">DSCR Com (10%)</option>\r\n <option value=\"commercial\">Commercial (10%)</option>\r\n <option value=\"mixed_use\">Mixed Use (10%)</option>\r\n <option value=\"rv_park\">RV Park (11%)</option>\r\n </select>\r\n <button class=\"btn-primary\" id=\"ln-export-btn\" title=\"Open dashboard with property data\">\r\n <svg class=\"icon\" viewBox=\"0 0 24 24\">\r\n <path d=\"M14,2H6A2,2 0 0,0 4,4V20A2,2 0 0,0 6,22H18A2,2 0 0,0 20,20V8L14,2M18,20H6V4H13V9H18V20Z\" />\r\n </svg>\r\n Dashboard\r\n </button>\r\n </div>\r\n </div>\r\n </div>\r\n `;\r\n\r\n try {\r\n if (document.body) {\r\n document.body.appendChild(footer);\r\n } else if (document.documentElement) {\r\n document.documentElement.appendChild(footer);\r\n }\r\n } catch (error) {\r\n // Silent fail\r\n }\r\n\r\n const exportBtn = document.getElementById(\"ln-export-btn\");\r\n if (exportBtn) {\r\n exportBtn.addEventListener(\"click\", () => {\r\n callbacks.onExportClick?.();\r\n });\r\n }\r\n\r\n const propertyTypeDropdown = document.getElementById(\"ln-property-type\");\r\n if (propertyTypeDropdown) {\r\n propertyTypeDropdown.addEventListener(\"change\", () => {\r\n callbacks.onPropertyTypeChange?.(propertyTypeDropdown.value);\r\n });\r\n }\r\n\r\n const interestRateTypeDropdown = document.getElementById(\"ln-interest-rate-type\");\r\n if (interestRateTypeDropdown) {\r\n interestRateTypeDropdown.addEventListener(\"change\", () => {\r\n updateState({ currentInterestRateType: interestRateTypeDropdown.value });\r\n callbacks.onInterestRateTypeChange?.(interestRateTypeDropdown.value);\r\n });\r\n }\r\n\r\n const unitsInput = document.getElementById(\"ln-units-input\");\r\n if (unitsInput) {\r\n unitsInput.addEventListener(\"change\", () => {\r\n const value = parseInt(unitsInput.value) || 4;\r\n updateState({ numberOfUnits: value });\r\n\r\n const irDropdown = document.getElementById(\"ln-interest-rate-type\");\r\n if (irDropdown) {\r\n if (value > 11 && irDropdown.value !== \"dscr_commercial\") {\r\n irDropdown.value = \"dscr_commercial\";\r\n updateState({ currentInterestRateType: \"dscr_commercial\" });\r\n callbacks.onInterestRateTypeChange?.(\"dscr_commercial\");\r\n } else if (value <= 11 && irDropdown.value === \"dscr_commercial\") {\r\n irDropdown.value = \"dscr_residential\";\r\n updateState({ currentInterestRateType: \"dscr_residential\" });\r\n callbacks.onInterestRateTypeChange?.(\"dscr_residential\");\r\n }\r\n }\r\n });\r\n }\r\n\r\n return footer;\r\n}\r\n"],"names":["PROPERTY_TYPE_OPTIONS","value","label","createPanel","config","cssUrls","defaultPropertyType","callbacks","console","log","existing","document","getElementById","remove","links","map","href","link","createElement","rel","loadedCount","total","length","onLoad","createPanelElements","forEach","onload","setTimeout","head","appendChild","updateState","footer","id","className","innerHTML","join","renderPropertyTypeOptions","body","documentElement","error","exportBtn","addEventListener","onExportClick","propertyTypeDropdown","onPropertyTypeChange","interestRateTypeDropdown","currentInterestRateType","onInterestRateTypeChange","unitsInput","parseInt","numberOfUnits","irDropdown"],"mappings":"AAUA,MAAMA,EAAwB,CAC5B,CAAEC,MAAO,cAAeC,MAAO,eAC/B,CAAED,MAAO,MAAOC,MAAO,OACvB,CAAED,MAAO,WAAYC,MAAO,sBAC5B,CAAED,MAAO,WAAYC,MAAO,YAC5B,CAAED,MAAO,YAAaC,MAAO,aAC7B,CAAED,MAAO,UAAWC,MAAO,YAUtB,SAASC,YAAYC,GAC1B,MAAMC,QAAEA,EAAU,GAAEC,oBAAEA,EAAsB,cAAaC,UAAEA,EAAY,CAAA,GAAOH,GAAU,GAExFI,QAAQC,IAAI,2BAEZ,MAAMC,EAAWC,SAASC,eAAe,aACrCF,IACFF,QAAQC,IAAI,gCACZC,EAASG,UAGXL,QAAQC,IAAI,2BAEZ,MAAMK,EAAQT,EAAQU,IAAKC,IACzB,MAAMC,EAAON,SAASO,cAAc,QAGpC,OAFAD,EAAKE,IAAM,aACXF,EAAKD,KAAOA,EACLC,IAIT,IAAIG,EAAc,EAClB,MAAMC,EAAQP,EAAMQ,OACdC,OAAS,KACbH,IACAZ,QAAQC,IAAI,uBAAuBW,KAAeC,MAC9CD,IAAgBC,IAClBb,QAAQC,IAAI,8CACZe,oBAAoBlB,EAAqBC,KAI7CO,EAAMW,QAASR,IAAWA,EAAKS,OAASH,SAExCf,QAAQC,IAAI,oCAEZkB,WAAW,KACTnB,QAAQC,IAAI,gEACZe,oBAAoBlB,EAAqBC,IACxC,KAEHO,EAAMW,QAASR,IAAWN,SAASiB,KAAKC,YAAYZ,IACtD,CAEA,SAASO,oBAAoBlB,EAAqBC,GAEhD,GAAII,SAASC,eAAe,aAAc,OAE1C,MAAMkB,YAAEA,GAAgBvB,EAElBwB,EAASpB,SAASO,cAAc,OACtCa,EAAOC,GAAK,YACZD,EAAOE,UAAY,aAEnBF,EAAOG,UAAY,8iKA7DrB,SAAmC5B,GACjC,OAAON,EAAsBe,IAAI,EAAGd,QAAOC,WAElC,kBAAkBD,KADRA,IAAUK,EAAsB,YAAc,MACjBJ,cAC7CiC,KAAK,iBACV,CA2KcC,CAA0B9B,2zBAoBtC,IACMK,SAAS0B,KACX1B,SAAS0B,KAAKR,YAAYE,GACjBpB,SAAS2B,iBAClB3B,SAAS2B,gBAAgBT,YAAYE,EAEzC,CAAE,MAAOQ,GAET,CAEA,MAAMC,EAAY7B,SAASC,eAAe,iBACtC4B,GACFA,EAAUC,iBAAiB,QAAS,KAClClC,EAAUmC,oBAId,MAAMC,EAAuBhC,SAASC,eAAe,oBACjD+B,GACFA,EAAqBF,iBAAiB,SAAU,KAC9ClC,EAAUqC,uBAAuBD,EAAqB1C,SAI1D,MAAM4C,EAA2BlC,SAASC,eAAe,yBACrDiC,GACFA,EAAyBJ,iBAAiB,SAAU,KAClDX,EAAY,CAAEgB,wBAAyBD,EAAyB5C,QAChEM,EAAUwC,2BAA2BF,EAAyB5C,SAIlE,MAAM+C,EAAarC,SAASC,eAAe,kBAqB3C,OApBIoC,GACFA,EAAWP,iBAAiB,SAAU,KACpC,MAAMxC,EAAQgD,SAASD,EAAW/C,QAAU,EAC5C6B,EAAY,CAAEoB,cAAejD,IAE7B,MAAMkD,EAAaxC,SAASC,eAAe,yBACvCuC,IACElD,EAAQ,IAA2B,oBAArBkD,EAAWlD,OAC3BkD,EAAWlD,MAAQ,kBACnB6B,EAAY,CAAEgB,wBAAyB,oBACvCvC,EAAUwC,2BAA2B,oBAC5B9C,GAAS,IAA2B,oBAArBkD,EAAWlD,QACnCkD,EAAWlD,MAAQ,mBACnB6B,EAAY,CAAEgB,wBAAyB,qBACvCvC,EAAUwC,2BAA2B,wBAMtChB,CACT"}
@@ -1,2 +1,2 @@
1
- import{createNavigationGuard as e}from"./createNavigationGuard.js";import{createPanel as t}from"./createPanel.js";import{runReveals as a}from"./runReveals.js";import{setupPriceClickHandler as n,setupCapRateClickHandler as r,setupDownPaymentClickHandler as o,setupDiscountButtonHandler as c}from"../ui/click-handlers.js";import{calculateFinancials as l}from"../financial/calculateFinancials.js";import{calculateDOM as i}from"../../date/utilities.js";function createPipeline({adapter:s,config:p,ctx:u,exportOps:d,finance:m,render:y,resolveCssUrls:g,services:f}){const{state:b,updateState:E}=u,listingId=()=>s.getListingId(window.location.href);async function updateFooterData(){const t=e(listingId);if(t.capture(),p.reveals?.length&&(await a(p.reveals),t.isStale()))return;const s=m.scrapeAndApply();if(!s)return console.error("❌ Malformed listing data — missing a contract field, refusing to render"),void y.updateElement("prop-name","Data error — see console");const d=s.unitCount??4;E({numberOfUnits:d});const g=document.getElementById("ln-units-input");if(g&&(g.value=d),d>11){const e=document.getElementById("ln-interest-rate-type");e&&"dscr_commercial"!==e.value&&(e.value="dscr_commercial",E({currentInterestRateType:"dscr_commercial"}))}y.updateElement("prop-name",s.name),y.updateElement("prop-price",b.priceWasDefaulted?"No price":s.price),y.updateElement("prop-contact",s.contact),y.updateElement("prop-phone",s.phone),y.updateElement("prop-dom",i(s.listingDate)),y.updatePriceLabel(),y.updateCapRateLabel(),y.syncUnitsFieldForType(b.currentPropertyType,s.bedroomCount),function(e){const t=document.getElementById("prop-name");t&&e.name&&"Not found"!==e.name&&(t.style.cursor="pointer",t.style.textDecoration="underline",t.onclick=()=>{const t=`https://www.google.com/maps/search/${encodeURIComponent(e.name)}`;window.open(t,"_blank")});const a={getCurrentPrice:y.getCurrentPrice,recalculateFinancials:m.recalculateFinancials,state:b,updatePercentageLabels:y.updatePercentageLabels,updatePriceLabel:y.updatePriceLabel,updateState:E},l=document.getElementById("prop-price");n(l,l?.closest(".metric")?.querySelector(".metric-label"),a);const i=document.getElementById("prop-cap");r(i,i?.closest(".metric")?.querySelector(".metric-label"),a);const s=document.getElementById("prop-down");o(s,s?.closest(".metric")?.querySelector(".metric-label"),a),c(document.getElementById("ln-discount-btn"),a)}(s);const C=b.isUsingEstimatedCapRate?`${b.currentEstimatedCapRate}%`:s.capRate,P=await l(u,s.price,C,b.currentPropertyType,s.name);if(t.isStale())return;y.applyFinancials(P),y.updateActiveCapDisplay();const w=await f.loadLeadStatus(s.name);if(t.isStale())return;y.updateElement("prop-lead-status",w.leadStatus),y.updateLeadStatusTooltip(w);const S=await f.loadStrValue(s.name,t);t.isStale()||S&&"str"===b.currentPropertyType&&(E({baseNOI:null}),await m.recalculateFinancials(),t.isStale())||(await f.loadDebt(s.name,t),t.isStale()||y.updateEquityDisplay())}let C=!1,P=null;return{runPipeline:function(){C=!1,P&&(P.disconnect(),P=null),t({callbacks:{onExportClick:d.handleExportClick,onInterestRateTypeChange:()=>m.recalculateFinancials(),onPropertyTypeChange:()=>{m.handlePropertyTypeChange(),y.updateCapRateLabel();const e=s.scrape();y.syncUnitsFieldForType(b.currentPropertyType,e?.bedroomCount),m.recalculateFinancials()},state:b,updateState:E},cssUrls:g(p.cssFiles),defaultPropertyType:p.defaultPropertyType});const runUpdateOnce=async()=>{C||(C=!0,await updateFooterData())},tryImmediateUpdate=()=>{const e=document.getElementById("prop-name"),t=document.getElementById("prop-price");return!!(e&&t&&e.textContent.trim()&&t.textContent.trim())&&(runUpdateOnce(),!0)};tryImmediateUpdate()||(P=new MutationObserver((e,t)=>{tryImmediateUpdate()&&(t.disconnect(),P=null)}),P.observe(document.body,{childList:!0,subtree:!0}),"complete"!==document.readyState&&window.addEventListener("load",()=>{setTimeout(()=>{C||(runUpdateOnce(),P&&(P.disconnect(),P=null))},5e3)}))},updateFooterData:updateFooterData}}export{createPipeline};
1
+ import{createNavigationGuard as e}from"./createNavigationGuard.js";import{createPanel as t}from"./createPanel.js";import{runReveals as a}from"./runReveals.js";import{setupPriceClickHandler as n,setupCapRateClickHandler as r,setupDownPaymentClickHandler as o,setupNoiClickHandler as c,setupAwningLinkHandler as l,setupDiscountButtonHandler as i}from"../ui/click-handlers.js";import{calculateFinancials as s}from"../financial/calculateFinancials.js";import{calculateDOM as p}from"../../date/utilities.js";function createPipeline({adapter:u,config:d,ctx:m,exportOps:y,finance:g,render:f,resolveCssUrls:E,services:b}){const{state:w,updateState:C}=m,listingId=()=>u.getListingId(window.location.href);async function updateFooterData(){const t=e(listingId);if(t.capture(),d.reveals?.length&&(await a(d.reveals),t.isStale()))return;const u=g.scrapeAndApply();if(!u)return console.error("❌ Malformed listing data — missing a contract field, refusing to render"),void f.updateElement("prop-name","Data error — see console");const y=u.unitCount??4;C({numberOfUnits:y});const E=document.getElementById("ln-units-input");if(E&&(E.value=y),y>11){const e=document.getElementById("ln-interest-rate-type");e&&"dscr_commercial"!==e.value&&(e.value="dscr_commercial",C({currentInterestRateType:"dscr_commercial"}))}f.updateElement("prop-name",u.name),f.updateElement("prop-price",w.priceWasDefaulted?"No price":u.price),f.updateElement("prop-contact",u.contact),f.updateElement("prop-phone",u.phone),f.updateElement("prop-dom",p(u.listingDate)),f.updatePriceLabel(),f.updateCapRateLabel(),f.syncUnitsFieldForType(w.currentPropertyType,u.bedroomCount),function(e){const t=document.getElementById("prop-name");t&&e.name&&"Not found"!==e.name&&(t.style.cursor="pointer",t.style.textDecoration="underline",t.onclick=()=>{const t=`https://www.google.com/maps/search/${encodeURIComponent(e.name)}`;window.open(t,"_blank")});const a={getCurrentPrice:f.getCurrentPrice,recalculateFinancials:g.recalculateFinancials,state:w,updatePercentageLabels:f.updatePercentageLabels,updatePriceLabel:f.updatePriceLabel,updateState:C},s=document.getElementById("prop-price");n(s,s?.closest(".metric")?.querySelector(".metric-label"),a);const p=document.getElementById("prop-cap");r(p,p?.closest(".metric")?.querySelector(".metric-label"),a);const u=document.getElementById("prop-down");o(u,u?.closest(".metric")?.querySelector(".metric-label"),a);const d=document.getElementById("prop-noi");c(d,d?.closest(".metric")?.querySelector(".metric-label"),a),l(document.getElementById("prop-noi-awning")),i(document.getElementById("ln-discount-btn"),a)}(u);const P=w.isUsingEstimatedCapRate?`${w.currentEstimatedCapRate}%`:u.capRate,S=await s(m,u.price,P,w.currentPropertyType,u.name);if(t.isStale())return;f.applyFinancials(S),f.updateActiveCapDisplay();const F=await b.loadLeadStatus(u.name);if(t.isStale())return;f.updateElement("prop-lead-status",F.leadStatus),f.updateLeadStatusTooltip(F);const I=await b.loadStrValue(u.name,t);t.isStale()||I&&"str"===w.currentPropertyType&&(C({baseNOI:null}),await g.recalculateFinancials(),t.isStale())||(await b.loadDebt(u.name,t),t.isStale()||f.updateEquityDisplay())}let P=!1,S=null;return{runPipeline:function(){P=!1,S&&(S.disconnect(),S=null),t({callbacks:{onExportClick:y.handleExportClick,onInterestRateTypeChange:()=>g.recalculateFinancials(),onPropertyTypeChange:()=>{g.handlePropertyTypeChange(),f.updateCapRateLabel();const e=u.scrape();f.syncUnitsFieldForType(w.currentPropertyType,e?.bedroomCount),g.recalculateFinancials()},state:w,updateState:C},cssUrls:E(d.cssFiles),defaultPropertyType:d.defaultPropertyType});const runUpdateOnce=async()=>{P||(P=!0,await updateFooterData())},tryImmediateUpdate=()=>{const e=document.getElementById("prop-name"),t=document.getElementById("prop-price");return!!(e&&t&&e.textContent.trim()&&t.textContent.trim())&&(runUpdateOnce(),!0)};tryImmediateUpdate()||(S=new MutationObserver((e,t)=>{tryImmediateUpdate()&&(t.disconnect(),S=null)}),S.observe(document.body,{childList:!0,subtree:!0}),"complete"!==document.readyState&&window.addEventListener("load",()=>{setTimeout(()=>{P||(runUpdateOnce(),S&&(S.disconnect(),S=null))},5e3)}))},updateFooterData:updateFooterData}}export{createPipeline};
2
2
  //# sourceMappingURL=pipeline.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"pipeline.js","sources":["../../../src/browser/widget/pipeline.js"],"sourcesContent":["// Pipeline unit: the re-runnable footer pipeline — builds the shared panel, wires the clickable\r\n// elements, runs the main async update (scrape -> financials -> lead status -> STR -> equity with\r\n// the navigation guard between awaits), and drives the immediate/observer/load entry points.\r\n// Extracted verbatim from createAnalyzer (T12).\r\n\r\nimport { createNavigationGuard } from \"./createNavigationGuard.js\";\r\nimport { createPanel } from \"./createPanel.js\";\r\nimport { runReveals } from \"./runReveals.js\";\r\nimport {\r\n setupCapRateClickHandler,\r\n setupDiscountButtonHandler,\r\n setupDownPaymentClickHandler,\r\n setupPriceClickHandler,\r\n} from \"../ui/click-handlers.js\";\r\nimport { calculateFinancials } from \"../financial/calculateFinancials.js\";\r\nimport { calculateDOM } from \"../../date/utilities.js\";\r\n\r\nexport function createPipeline({ adapter, config, ctx, exportOps, finance, render, resolveCssUrls, services }) {\r\n const { state, updateState } = ctx;\r\n const listingId = () => adapter.getListingId(window.location.href);\r\n\r\n function setupClickableElements(data) {\r\n const nameElement = document.getElementById(\"prop-name\");\r\n if (nameElement && data.name && data.name !== \"Not found\") {\r\n nameElement.style.cursor = \"pointer\";\r\n nameElement.style.textDecoration = \"underline\";\r\n nameElement.onclick = () => {\r\n const searchUrl = `https://www.google.com/maps/search/${encodeURIComponent(data.name)}`;\r\n window.open(searchUrl, \"_blank\");\r\n };\r\n }\r\n\r\n // The shared click-handlers read callbacks.state / callbacks.updateState — the engine's\r\n // ctx is injected here (no global-state coupling).\r\n const callbacks = {\r\n getCurrentPrice: render.getCurrentPrice,\r\n recalculateFinancials: finance.recalculateFinancials,\r\n state,\r\n updatePercentageLabels: render.updatePercentageLabels,\r\n updatePriceLabel: render.updatePriceLabel,\r\n updateState,\r\n };\r\n\r\n const priceElement = document.getElementById(\"prop-price\");\r\n setupPriceClickHandler(priceElement, priceElement?.closest(\".metric\")?.querySelector(\".metric-label\"), callbacks);\r\n\r\n const capElement = document.getElementById(\"prop-cap\");\r\n setupCapRateClickHandler(capElement, capElement?.closest(\".metric\")?.querySelector(\".metric-label\"), callbacks);\r\n\r\n const downElement = document.getElementById(\"prop-down\");\r\n setupDownPaymentClickHandler(downElement, downElement?.closest(\".metric\")?.querySelector(\".metric-label\"), callbacks);\r\n\r\n setupDiscountButtonHandler(document.getElementById(\"ln-discount-btn\"), callbacks);\r\n }\r\n\r\n async function updateFooterData() {\r\n // The listing this run is for. On an SPA the page can navigate mid-flight; after each\r\n // await we drop out if the identity changed, so a stale run never writes onto another\r\n // listing's panel. On a full-reload site getListingId is stable, so isStale() is always\r\n // false and this is a no-op.\r\n const guard = createNavigationGuard(listingId);\r\n guard.capture();\r\n\r\n // Click-to-reveal any data gated behind a button (broker phone/email, OM access) so the\r\n // pure scrape() below reads it. Platform-declared (config.reveals); a no-op when absent.\r\n if (config.reveals?.length) {\r\n await runReveals(config.reveals);\r\n if (guard.isStale()) return;\r\n }\r\n\r\n const data = finance.scrapeAndApply();\r\n if (!data) {\r\n console.error(\"❌ Malformed listing data — missing a contract field, refusing to render\");\r\n render.updateElement(\"prop-name\", \"Data error — see console\");\r\n return;\r\n }\r\n\r\n const unitCount = data.unitCount ?? 4;\r\n updateState({ numberOfUnits: unitCount });\r\n const unitsInput = document.getElementById(\"ln-units-input\");\r\n if (unitsInput) unitsInput.value = unitCount;\r\n\r\n if (unitCount > 11) {\r\n const irDropdown = document.getElementById(\"ln-interest-rate-type\");\r\n if (irDropdown && irDropdown.value !== \"dscr_commercial\") {\r\n irDropdown.value = \"dscr_commercial\";\r\n updateState({ currentInterestRateType: \"dscr_commercial\" });\r\n }\r\n }\r\n\r\n render.updateElement(\"prop-name\", data.name);\r\n // Display guard (H2): a defaulted price shows \"No price\"; the metrics fall through to N/A.\r\n render.updateElement(\"prop-price\", state.priceWasDefaulted ? \"No price\" : data.price);\r\n render.updateElement(\"prop-contact\", data.contact);\r\n render.updateElement(\"prop-phone\", data.phone);\r\n render.updateElement(\"prop-dom\", calculateDOM(data.listingDate));\r\n\r\n render.updatePriceLabel();\r\n render.updateCapRateLabel();\r\n render.syncUnitsFieldForType(state.currentPropertyType, data.bedroomCount);\r\n setupClickableElements(data);\r\n\r\n const calculationCapRate = state.isUsingEstimatedCapRate ? `${state.currentEstimatedCapRate}%` : data.capRate;\r\n const financials = await calculateFinancials(ctx, data.price, calculationCapRate, state.currentPropertyType, data.name);\r\n if (guard.isStale()) return;\r\n render.applyFinancials(financials);\r\n render.updateActiveCapDisplay();\r\n\r\n const loiData = await services.loadLeadStatus(data.name);\r\n if (guard.isStale()) return;\r\n render.updateElement(\"prop-lead-status\", loiData.leadStatus);\r\n render.updateLeadStatusTooltip(loiData);\r\n\r\n // STR revenue seam: the footer already shows the 5.5%-of-price estimate. If the backend\r\n // returns real data, recompute the STR NOI with it. Dormant until that backend ships.\r\n const strResult = await services.loadStrValue(data.name, guard);\r\n if (guard.isStale()) return;\r\n if (strResult && state.currentPropertyType === \"str\") {\r\n updateState({ baseNOI: null });\r\n await finance.recalculateFinancials();\r\n if (guard.isStale()) return;\r\n }\r\n\r\n await services.loadDebt(data.name, guard);\r\n if (guard.isStale()) return;\r\n render.updateEquityDisplay();\r\n }\r\n\r\n // One running pipeline at a time. Re-runnable so the SPA watcher can rebuild per listing;\r\n // the observer is tracked so a re-run detaches the previous one.\r\n let footerUpdated = false;\r\n let pipelineObserver = null;\r\n\r\n function runPipeline() {\r\n footerUpdated = false;\r\n if (pipelineObserver) {\r\n pipelineObserver.disconnect();\r\n pipelineObserver = null;\r\n }\r\n\r\n createPanel({\r\n callbacks: {\r\n onExportClick: exportOps.handleExportClick,\r\n onInterestRateTypeChange: () => finance.recalculateFinancials(),\r\n onPropertyTypeChange: () => {\r\n finance.handlePropertyTypeChange();\r\n render.updateCapRateLabel();\r\n const listing = adapter.scrape();\r\n render.syncUnitsFieldForType(state.currentPropertyType, listing?.bedroomCount);\r\n finance.recalculateFinancials();\r\n },\r\n state,\r\n updateState,\r\n },\r\n cssUrls: resolveCssUrls(config.cssFiles),\r\n defaultPropertyType: config.defaultPropertyType,\r\n });\r\n\r\n const runUpdateOnce = async () => {\r\n if (footerUpdated) return;\r\n footerUpdated = true;\r\n await updateFooterData();\r\n };\r\n\r\n const tryImmediateUpdate = () => {\r\n const nameEl = document.getElementById(\"prop-name\");\r\n const priceEl = document.getElementById(\"prop-price\");\r\n if (nameEl && priceEl && nameEl.textContent.trim() && priceEl.textContent.trim()) {\r\n runUpdateOnce();\r\n return true;\r\n }\r\n return false;\r\n };\r\n\r\n if (tryImmediateUpdate()) return;\r\n\r\n pipelineObserver = new MutationObserver((mutations, obs) => {\r\n if (tryImmediateUpdate()) {\r\n obs.disconnect();\r\n pipelineObserver = null;\r\n }\r\n });\r\n pipelineObserver.observe(document.body, { childList: true, subtree: true });\r\n\r\n // Safety fallback before the page has loaded; SPA navigations fire after load, so the\r\n // observer (not load) drives those.\r\n if (document.readyState !== \"complete\") {\r\n window.addEventListener(\"load\", () => {\r\n setTimeout(() => {\r\n if (!footerUpdated) {\r\n runUpdateOnce();\r\n if (pipelineObserver) {\r\n pipelineObserver.disconnect();\r\n pipelineObserver = null;\r\n }\r\n }\r\n }, 5000);\r\n });\r\n }\r\n }\r\n\r\n return { runPipeline, updateFooterData };\r\n}\r\n"],"names":["createPipeline","adapter","config","ctx","exportOps","finance","render","resolveCssUrls","services","state","updateState","listingId","getListingId","window","location","href","async","updateFooterData","guard","createNavigationGuard","capture","reveals","length","runReveals","isStale","data","scrapeAndApply","console","error","updateElement","unitCount","numberOfUnits","unitsInput","document","getElementById","value","irDropdown","currentInterestRateType","name","priceWasDefaulted","price","contact","phone","calculateDOM","listingDate","updatePriceLabel","updateCapRateLabel","syncUnitsFieldForType","currentPropertyType","bedroomCount","nameElement","style","cursor","textDecoration","onclick","searchUrl","encodeURIComponent","open","callbacks","getCurrentPrice","recalculateFinancials","updatePercentageLabels","priceElement","setupPriceClickHandler","closest","querySelector","capElement","setupCapRateClickHandler","downElement","setupDownPaymentClickHandler","setupDiscountButtonHandler","setupClickableElements","calculationCapRate","isUsingEstimatedCapRate","currentEstimatedCapRate","capRate","financials","calculateFinancials","applyFinancials","updateActiveCapDisplay","loiData","loadLeadStatus","leadStatus","updateLeadStatusTooltip","strResult","loadStrValue","baseNOI","loadDebt","updateEquityDisplay","footerUpdated","pipelineObserver","runPipeline","disconnect","createPanel","onExportClick","handleExportClick","onInterestRateTypeChange","onPropertyTypeChange","handlePropertyTypeChange","listing","scrape","cssUrls","cssFiles","defaultPropertyType","runUpdateOnce","tryImmediateUpdate","nameEl","priceEl","textContent","trim","MutationObserver","mutations","obs","observe","body","childList","subtree","readyState","addEventListener","setTimeout"],"mappings":"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"}
1
+ {"version":3,"file":"pipeline.js","sources":["../../../src/browser/widget/pipeline.js"],"sourcesContent":["// Pipeline unit: the re-runnable footer pipeline — builds the shared panel, wires the clickable\r\n// elements, runs the main async update (scrape -> financials -> lead status -> STR -> equity with\r\n// the navigation guard between awaits), and drives the immediate/observer/load entry points.\r\n// Extracted verbatim from createAnalyzer (T12).\r\n\r\nimport { createNavigationGuard } from \"./createNavigationGuard.js\";\r\nimport { createPanel } from \"./createPanel.js\";\r\nimport { runReveals } from \"./runReveals.js\";\r\nimport {\r\n setupAwningLinkHandler,\r\n setupCapRateClickHandler,\r\n setupDiscountButtonHandler,\r\n setupDownPaymentClickHandler,\r\n setupNoiClickHandler,\r\n setupPriceClickHandler,\r\n} from \"../ui/click-handlers.js\";\r\nimport { calculateFinancials } from \"../financial/calculateFinancials.js\";\r\nimport { calculateDOM } from \"../../date/utilities.js\";\r\n\r\nexport function createPipeline({ adapter, config, ctx, exportOps, finance, render, resolveCssUrls, services }) {\r\n const { state, updateState } = ctx;\r\n const listingId = () => adapter.getListingId(window.location.href);\r\n\r\n function setupClickableElements(data) {\r\n const nameElement = document.getElementById(\"prop-name\");\r\n if (nameElement && data.name && data.name !== \"Not found\") {\r\n nameElement.style.cursor = \"pointer\";\r\n nameElement.style.textDecoration = \"underline\";\r\n nameElement.onclick = () => {\r\n const searchUrl = `https://www.google.com/maps/search/${encodeURIComponent(data.name)}`;\r\n window.open(searchUrl, \"_blank\");\r\n };\r\n }\r\n\r\n // The shared click-handlers read callbacks.state / callbacks.updateState — the engine's\r\n // ctx is injected here (no global-state coupling).\r\n const callbacks = {\r\n getCurrentPrice: render.getCurrentPrice,\r\n recalculateFinancials: finance.recalculateFinancials,\r\n state,\r\n updatePercentageLabels: render.updatePercentageLabels,\r\n updatePriceLabel: render.updatePriceLabel,\r\n updateState,\r\n };\r\n\r\n const priceElement = document.getElementById(\"prop-price\");\r\n setupPriceClickHandler(priceElement, priceElement?.closest(\".metric\")?.querySelector(\".metric-label\"), callbacks);\r\n\r\n const capElement = document.getElementById(\"prop-cap\");\r\n setupCapRateClickHandler(capElement, capElement?.closest(\".metric\")?.querySelector(\".metric-label\"), callbacks);\r\n\r\n const downElement = document.getElementById(\"prop-down\");\r\n setupDownPaymentClickHandler(downElement, downElement?.closest(\".metric\")?.querySelector(\".metric-label\"), callbacks);\r\n\r\n const noiElement = document.getElementById(\"prop-noi\");\r\n setupNoiClickHandler(noiElement, noiElement?.closest(\".metric\")?.querySelector(\".metric-label\"), callbacks);\r\n setupAwningLinkHandler(document.getElementById(\"prop-noi-awning\"));\r\n\r\n setupDiscountButtonHandler(document.getElementById(\"ln-discount-btn\"), callbacks);\r\n }\r\n\r\n async function updateFooterData() {\r\n // The listing this run is for. On an SPA the page can navigate mid-flight; after each\r\n // await we drop out if the identity changed, so a stale run never writes onto another\r\n // listing's panel. On a full-reload site getListingId is stable, so isStale() is always\r\n // false and this is a no-op.\r\n const guard = createNavigationGuard(listingId);\r\n guard.capture();\r\n\r\n // Click-to-reveal any data gated behind a button (broker phone/email, OM access) so the\r\n // pure scrape() below reads it. Platform-declared (config.reveals); a no-op when absent.\r\n if (config.reveals?.length) {\r\n await runReveals(config.reveals);\r\n if (guard.isStale()) return;\r\n }\r\n\r\n const data = finance.scrapeAndApply();\r\n if (!data) {\r\n console.error(\"❌ Malformed listing data — missing a contract field, refusing to render\");\r\n render.updateElement(\"prop-name\", \"Data error — see console\");\r\n return;\r\n }\r\n\r\n const unitCount = data.unitCount ?? 4;\r\n updateState({ numberOfUnits: unitCount });\r\n const unitsInput = document.getElementById(\"ln-units-input\");\r\n if (unitsInput) unitsInput.value = unitCount;\r\n\r\n if (unitCount > 11) {\r\n const irDropdown = document.getElementById(\"ln-interest-rate-type\");\r\n if (irDropdown && irDropdown.value !== \"dscr_commercial\") {\r\n irDropdown.value = \"dscr_commercial\";\r\n updateState({ currentInterestRateType: \"dscr_commercial\" });\r\n }\r\n }\r\n\r\n render.updateElement(\"prop-name\", data.name);\r\n // Display guard (H2): a defaulted price shows \"No price\"; the metrics fall through to N/A.\r\n render.updateElement(\"prop-price\", state.priceWasDefaulted ? \"No price\" : data.price);\r\n render.updateElement(\"prop-contact\", data.contact);\r\n render.updateElement(\"prop-phone\", data.phone);\r\n render.updateElement(\"prop-dom\", calculateDOM(data.listingDate));\r\n\r\n render.updatePriceLabel();\r\n render.updateCapRateLabel();\r\n render.syncUnitsFieldForType(state.currentPropertyType, data.bedroomCount);\r\n setupClickableElements(data);\r\n\r\n const calculationCapRate = state.isUsingEstimatedCapRate ? `${state.currentEstimatedCapRate}%` : data.capRate;\r\n const financials = await calculateFinancials(ctx, data.price, calculationCapRate, state.currentPropertyType, data.name);\r\n if (guard.isStale()) return;\r\n render.applyFinancials(financials);\r\n render.updateActiveCapDisplay();\r\n\r\n const loiData = await services.loadLeadStatus(data.name);\r\n if (guard.isStale()) return;\r\n render.updateElement(\"prop-lead-status\", loiData.leadStatus);\r\n render.updateLeadStatusTooltip(loiData);\r\n\r\n // STR revenue seam: the footer already shows the 5.5%-of-price estimate. If the backend\r\n // returns real data, recompute the STR NOI with it. Dormant until that backend ships.\r\n const strResult = await services.loadStrValue(data.name, guard);\r\n if (guard.isStale()) return;\r\n if (strResult && state.currentPropertyType === \"str\") {\r\n updateState({ baseNOI: null });\r\n await finance.recalculateFinancials();\r\n if (guard.isStale()) return;\r\n }\r\n\r\n await services.loadDebt(data.name, guard);\r\n if (guard.isStale()) return;\r\n render.updateEquityDisplay();\r\n }\r\n\r\n // One running pipeline at a time. Re-runnable so the SPA watcher can rebuild per listing;\r\n // the observer is tracked so a re-run detaches the previous one.\r\n let footerUpdated = false;\r\n let pipelineObserver = null;\r\n\r\n function runPipeline() {\r\n footerUpdated = false;\r\n if (pipelineObserver) {\r\n pipelineObserver.disconnect();\r\n pipelineObserver = null;\r\n }\r\n\r\n createPanel({\r\n callbacks: {\r\n onExportClick: exportOps.handleExportClick,\r\n onInterestRateTypeChange: () => finance.recalculateFinancials(),\r\n onPropertyTypeChange: () => {\r\n finance.handlePropertyTypeChange();\r\n render.updateCapRateLabel();\r\n const listing = adapter.scrape();\r\n render.syncUnitsFieldForType(state.currentPropertyType, listing?.bedroomCount);\r\n finance.recalculateFinancials();\r\n },\r\n state,\r\n updateState,\r\n },\r\n cssUrls: resolveCssUrls(config.cssFiles),\r\n defaultPropertyType: config.defaultPropertyType,\r\n });\r\n\r\n const runUpdateOnce = async () => {\r\n if (footerUpdated) return;\r\n footerUpdated = true;\r\n await updateFooterData();\r\n };\r\n\r\n const tryImmediateUpdate = () => {\r\n const nameEl = document.getElementById(\"prop-name\");\r\n const priceEl = document.getElementById(\"prop-price\");\r\n if (nameEl && priceEl && nameEl.textContent.trim() && priceEl.textContent.trim()) {\r\n runUpdateOnce();\r\n return true;\r\n }\r\n return false;\r\n };\r\n\r\n if (tryImmediateUpdate()) return;\r\n\r\n pipelineObserver = new MutationObserver((mutations, obs) => {\r\n if (tryImmediateUpdate()) {\r\n obs.disconnect();\r\n pipelineObserver = null;\r\n }\r\n });\r\n pipelineObserver.observe(document.body, { childList: true, subtree: true });\r\n\r\n // Safety fallback before the page has loaded; SPA navigations fire after load, so the\r\n // observer (not load) drives those.\r\n if (document.readyState !== \"complete\") {\r\n window.addEventListener(\"load\", () => {\r\n setTimeout(() => {\r\n if (!footerUpdated) {\r\n runUpdateOnce();\r\n if (pipelineObserver) {\r\n pipelineObserver.disconnect();\r\n pipelineObserver = null;\r\n }\r\n }\r\n }, 5000);\r\n });\r\n }\r\n }\r\n\r\n return { runPipeline, updateFooterData };\r\n}\r\n"],"names":["createPipeline","adapter","config","ctx","exportOps","finance","render","resolveCssUrls","services","state","updateState","listingId","getListingId","window","location","href","async","updateFooterData","guard","createNavigationGuard","capture","reveals","length","runReveals","isStale","data","scrapeAndApply","console","error","updateElement","unitCount","numberOfUnits","unitsInput","document","getElementById","value","irDropdown","currentInterestRateType","name","priceWasDefaulted","price","contact","phone","calculateDOM","listingDate","updatePriceLabel","updateCapRateLabel","syncUnitsFieldForType","currentPropertyType","bedroomCount","nameElement","style","cursor","textDecoration","onclick","searchUrl","encodeURIComponent","open","callbacks","getCurrentPrice","recalculateFinancials","updatePercentageLabels","priceElement","setupPriceClickHandler","closest","querySelector","capElement","setupCapRateClickHandler","downElement","setupDownPaymentClickHandler","noiElement","setupNoiClickHandler","setupAwningLinkHandler","setupDiscountButtonHandler","setupClickableElements","calculationCapRate","isUsingEstimatedCapRate","currentEstimatedCapRate","capRate","financials","calculateFinancials","applyFinancials","updateActiveCapDisplay","loiData","loadLeadStatus","leadStatus","updateLeadStatusTooltip","strResult","loadStrValue","baseNOI","loadDebt","updateEquityDisplay","footerUpdated","pipelineObserver","runPipeline","disconnect","createPanel","onExportClick","handleExportClick","onInterestRateTypeChange","onPropertyTypeChange","handlePropertyTypeChange","listing","scrape","cssUrls","cssFiles","defaultPropertyType","runUpdateOnce","tryImmediateUpdate","nameEl","priceEl","textContent","trim","MutationObserver","mutations","obs","observe","body","childList","subtree","readyState","addEventListener","setTimeout"],"mappings":"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"}
@@ -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";const u=["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:d}){const{state:m,updateState:g}=d;function getCurrentPrice(){if(m.originalPrice&&m.currentPriceDiscount>0){const e=parseFloat(m.originalPrice.replace(/[$,]/g,""))*(1-m.currentPriceDiscount/100);return`$${Math.round(e).toLocaleString()}`}return m.originalPrice}function updatePercentageLabels(){const e=document.querySelector("#prop-down")?.closest(".metric")?.querySelector(".metric-label");e&&(e.textContent=`Down (${m.currentDownPaymentPercent}%)`);const t=document.querySelector("#prop-seller-fi")?.closest(".metric")?.querySelector(".metric-label");t&&(t.textContent=`Seller FI (${m.currentSellerFiPercent}%)`);const n=document.querySelector("#prop-dscr")?.closest(".metric")?.querySelector(".metric-label");n&&(n.textContent=`DSCR (${m.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,m.currentDownPaymentPercent,m.currentDSCRPercent,m.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 u.forEach(e=>updateElement(e,"N/A"));updatePercentageLabels();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)),g({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(m.baseNOI,c);const s=l(m.originalCapRate,m.isUsingEstimatedCapRate),p=`${"<strong>Reported cap rate:</strong> "+(null!=s?`${s}%`:"N/A")}${m.isUsingEstimatedCapRate?"<hr><em>Click the cap rate to increase by 1%; 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(m.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=m.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"===m.equitySource?"Public records (API)":m.equitySource;let d=`<strong>Debt owing:</strong> ${a?"Unknown":fmtUSD(s)}`;d+=`<br><strong>Source:</strong> ${u}`,m.cachedDebtAddress&&(d+=`<br><strong>Matched:</strong> ${m.cachedDebtAddress}`);const g=Array.isArray(m.cachedMortgages)?m.cachedMortgages:[];g.length&&(d+="<hr>"+g.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 f=r.closest(".metric");if(f){const r=f.querySelector(".metric-label");e(f)?n(f,d):(t(f,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=m.currentPriceDiscount>0?`Price (${m.currentPriceDiscount}%)`:"Price")}}}export{createRender};
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 &times; ${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")}${f.isUsingEstimatedCapRate?"<hr><em>Click the cap rate to increase by 1%; 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\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 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 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","footer","toggle","syncUnitsFieldForType","propertyType","bedroomCount","input","String","numberOfUnits","updateActiveCapDisplay","capElement","priceText","parsePriceNumber","computeActiveCapDisplay","baseNOI","reported","parseReportedCap","originalCapRate","isUsingEstimatedCapRate","updateCapRateLabel","capLabelElement","currentPropertyType","updateEquityDisplay","equityElement","debt","cachedDebtBalance","estimated","equity","equityPercentFromDebt","fmtUSD","n","Number","isFinite","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\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 &times; ${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":"+nBAgBA,MAAMA,EAAwB,CAC5B,WAAY,YAAa,WAAY,iBAAkB,eACvD,eAAgB,kBAAmB,YAAa,UAAW,iBAGtD,SAASC,cAAaC,IAAEA,IAC7B,MAAMC,MAAEA,EAAKC,YAAEA,GAAgBF,EAE/B,SAASG,kBACP,GAAIF,EAAMG,eAAiBH,EAAMI,qBAAuB,EAAG,CACzD,MACMC,EADeC,WAAWN,EAAMG,cAAcI,QAAQ,QAAS,MAC7B,EAAIP,EAAMI,qBAAuB,KACzE,MAAO,IAAII,KAAKC,MAAMJ,GAAiBK,kBACzC,CACA,OAAOV,EAAMG,aACf,CASA,SAASQ,yBACP,MAAMC,EAAmBC,SAASC,cAAc,eAAeC,QAAQ,YAAYD,cAAc,iBAC7FF,IAAkBA,EAAiBI,YAAc,SAAShB,EAAMiB,+BAEpE,MAAMC,EAAuBL,SAASC,cAAc,oBAAoBC,QAAQ,YAAYD,cAAc,iBACtGI,IAAsBA,EAAqBF,YAAc,cAAchB,EAAMmB,4BAEjF,MAAMC,EAAmBP,SAASC,cAAc,eAAeC,QAAQ,YAAYD,cAAc,iBAC7FM,IAAkBA,EAAiBJ,YAAc,SAAShB,EAAMqB,uBACtE,CAEA,SAASC,cAAcC,EAAIC,GACzB,MAAMC,EAAUZ,SAASa,eAAeH,GACpCE,IAASA,EAAQT,YAAcQ,EACrC,CAsOA,MAAO,CACLG,gBAhCF,SAAyBC,GACvB,IAAIC,GAAqB,EACzB,GAAID,EAAY,CACd,MAAME,EAAW,CACf,WAAYF,EAAWG,IACvB,YAAaH,EAAWI,KACxB,WAAYJ,EAAWK,WACvB,iBAAkBL,EAAWM,SAC7B,eAAgBN,EAAWO,OAC3B,eAAgBP,EAAWQ,eAC3B,kBAAmBR,EAAWS,WAC9B,YAAaT,EAAWU,KACxB,UAAWV,EAAWW,UACtB,gBAAiBX,EAAWY,UAE9BX,EAAqBD,EAAWa,YAAc,EAC9C,IAAK,MAAOlB,EAAIC,KAAUkB,OAAOC,QAAQb,GAAWR,cAAcC,EAAIC,GACtEoB,WAAW,MAhGf,WACE,MAAMC,EAAchC,SAASa,eAAe,aACtCoB,EAAejC,SAASa,eAAe,cACvCqB,EAAalC,SAASa,eAAe,YAC3C,IAAKmB,IAAgBC,IAAiBC,EAAY,OAElD,MAAMC,EAAgBC,EAAmBH,EAAa9B,YAAa+B,EAAW/B,aAC9E,GAAIgC,EAAe,CACjB,MAAME,EAAiBC,EACrBH,EAAcI,MACdJ,EAAcjB,IACd/B,EAAMiB,0BACNjB,EAAMqB,mBACNrB,EAAMmB,wBAEFkC,EAASR,EAAY9B,QAAQ,WAC/BsC,IACGC,EAAWD,GACXE,EAAqBF,EAAQH,GADTM,EAAcH,EAAQH,GAGnD,CACF,CA4EMO,GA1EN,WACE,MAAMC,EAAkB7C,SAASa,eAAe,iBAC1CoB,EAAejC,SAASa,eAAe,cAC7C,IAAKgC,IAAoBZ,EAAc,OAEvC,MAAMa,EAAeC,EAAkBd,EAAa9B,YAAa0C,EAAgB1C,aACjF,GAAI2C,EAAc,CAChB,MAAMT,EAAiBW,EAA4BF,EAAaP,MAAOO,EAAaG,iBAC9ET,EAASK,EAAgB3C,QAAQ,WACvC,GAAIsC,EAAQ,CACV,MAAMU,EAAQV,EAAOvC,cAAc,iBAC/BiD,GAAOA,EAAMC,UAAUC,IAAI,eAC1BX,EAAWD,GACXE,EAAqBF,EAAQH,GADTM,EAAcH,EAAQH,EAEjD,CACF,CACF,CA2DMgB,IACC,IACL,MACErE,EAAsBsE,QAAS5C,GAAOD,cAAcC,EAAI,QAG1DZ,yBA3DF,WACE,MAAMyD,EAAsC,QAA9BpE,EAAMqE,oBAEdC,EAAazD,SAASa,eAAe,mBACvC4C,IAAYA,EAAWC,MAAMC,QAAUJ,EAAQ,eAAiB,QAEpE,MAAMrB,EAAalC,SAASa,eAAe,YAC3C,IAAKqB,EAAY,OAEjB,GADAA,EAAWwB,MAAME,OAASL,EAAQ,UAAY,GAC1CrB,EAAWjC,cAAc,SAAU,OAEvC,MAAMuC,EAASN,EAAWhC,QAAQ,WAGlC,GAFeqD,GAASpE,EAAM0E,gBAAkBC,OAAOC,SAAS5E,EAAM0E,eAAelD,OAEzE,CACVuB,EAAW/B,YAAc,GAAG+B,EAAW/B,eACvC,MAAM6D,EAAQ7E,EAAM0E,eAAelD,MAC7BsD,EAAMC,EAAwBC,IAAIC,eAClCC,EAAM,+CAA+CC,EAAeN,gCAC3CrE,KAAKC,MAAY,IAANqE,SAAiBK,EAAeN,EAAQC,8EAE9EzB,IACGC,EAAWD,GAIdE,EAAqBF,EAAQ6B,IAH7B1B,EAAcH,EAAQ6B,GACtB7B,EAAOvC,cAAc,kBAAkBkD,UAAUC,IAAI,gBAK3D,MAAWG,GAASf,GAAUC,EAAWD,IACvCE,EAAqBF,EAAQ,2GAEjC,CA4BE+B,GACA,MAAMC,EAASxE,SAASa,eAAe,aACnC2D,GAAQA,EAAOrB,UAAUsB,OAAO,WAAYzD,EAClD,EAIE3B,gCACAqF,sBAjJF,SAA+BC,EAAcC,GAC3C,MAAM1B,EAAQlD,SAASC,cAAc,uBAErC,GADIiD,IAAOA,EAAM/C,YAA+B,aAAjBwE,EAA8B,OAAS,SACjD,aAAjBA,GAA+C,MAAhBC,EAAsB,CACvD,MAAMC,EAAQ7E,SAASa,eAAe,kBAClCgE,IAAOA,EAAMlE,MAAQmE,OAAOF,IAChCxF,EAAY,CAAE2F,cAAeH,GAC/B,CACF,EA0IEI,uBAxNF,WACE,MAAMC,EAAajF,SAASa,eAAe,YAC3C,IAAKoE,EAAY,OAEjB,MAAMC,EAAY7F,mBAAqBW,SAASa,eAAe,eAAeV,aAAe,GACvFoC,EAAQ4C,EAAiBD,GAC/BD,EAAW9E,YAAciF,EAAwBjG,EAAMkG,QAAS9C,GAEhE,MAAM+C,EAAWC,EAAiBpG,EAAMqG,gBAAiBrG,EAAMsG,yBAKzDpD,EAAiB,GAJF,wCAAmD,MAAZiD,EAAmB,GAAGA,KAAc,SAC9EnG,EAAMsG,wBACpB,8EACA,KAGEjD,EAASyC,EAAW/E,QAAQ,WAClC,GAAIsC,EAAQ,CACV,MAAMU,EAAQV,EAAOvC,cAAc,iBAC9BwC,EAAWD,GAIdE,EAAqBF,EAAQH,IAH7BM,EAAcH,EAAQH,GAClBa,GAAOA,EAAMC,UAAUC,IAAI,eAInC,CACF,EAgMEsC,mBAzOF,WACE,MAAMC,EAAkB3F,SAASC,cAAc,cAAcC,QAAQ,YAAYD,cAAc,iBAC/F,GAAK0F,EACL,OAAQxG,EAAMqE,qBACZ,IAAK,MAAOmC,EAAgBxF,YAAc,iBAAkB,MAC5D,IAAK,WAAYwF,EAAgBxF,YAAc,sBAAuB,MACtE,QAASwF,EAAgBxF,YAAc,WAE3C,EAkOEM,4BACAmF,oBA7LF,WACE,MAAMC,EAAgB7F,SAASa,eAAe,eAC9C,IAAKgF,EAAe,OAEpB,MAAMX,EAAY7F,mBAAqBW,SAASa,eAAe,eAAeV,aAAe,GACvFoC,EAAQ4C,EAAiBD,GACzBY,EAAO3G,EAAM4G,kBACbC,EAAYF,QACZG,EAASC,EAAsB3D,EAAOuD,GAC5CD,EAAc1F,YAAc,GAAGR,KAAKC,MAAe,IAATqG,MAAiBD,EAAY,IAAM,KAE7E,MAAMG,OAAUC,GAAOtC,OAAOC,SAASD,OAAOsC,IAAM,IAAIzG,KAAKC,MAAMkE,OAAOsC,IAAIvG,mBAAqB,MAC7FwG,EAAcL,EAChB,iDACwB,YAAvB7G,EAAMmH,aAA6B,uBAAyBnH,EAAMmH,aAEvE,IAAIC,EAAU,gCAAgCP,EAAY,UAAYG,OAAOL,KAC7ES,GAAW,gCAAgCF,IACvClH,EAAMqH,oBAAmBD,GAAW,iCAAiCpH,EAAMqH,qBAE/E,MAAMC,EAAYC,MAAMC,QAAQxH,EAAMyH,iBAAmBzH,EAAMyH,gBAAkB,GAC7EH,EAAUI,SACZN,GAAW,OAASE,EAAUK,IAAKC,IACjC,MAAMC,EAA0B,MAAlBD,EAAEE,cAA2C,KAAnBF,EAAEE,aAAuB,MAAMF,EAAEE,gBAAkB,GAC3F,MAAO,WAAWF,EAAEG,UAAY,oBAAoBf,OAAOY,EAAEI,WAAWJ,EAAEK,UAAY,KAAKJ,MAASD,EAAEM,YAAc,SACnHC,KAAK,SAGV,MAAM9E,EAASqD,EAAc3F,QAAQ,WACrC,GAAIsC,EAAQ,CACV,MAAMU,EAAQV,EAAOvC,cAAc,iBAC9BwC,EAAWD,GAIdE,EAAqBF,EAAQ+D,IAH7B5D,EAAcH,EAAQ+D,GAClBrD,GAAOA,EAAMC,UAAUC,IAAI,eAInC,CACF,EAwJEmE,wBA5IF,SAAiCC,GAC/B,MAAMC,EAAczH,SAASa,eAAe,oBAC5C,IAAK4G,EAAa,OAClB,IAAIpF,EAAiB,uBACjBmF,IACFnF,EAAiB,qCACWmF,EAAQE,4BAClCF,EAAQG,4BAGZ,MAAMnF,EAASiF,EAAYvH,QAAQ,WACnC,GAAIsC,EACF,GAAKC,EAAWD,GAKdE,EAAqBF,EAAQH,OALN,CACvBM,EAAcH,EAAQH,GACtB,MAAMa,EAAQV,EAAOvC,cAAc,iBAC/BiD,GAAOA,EAAMC,UAAUC,IAAI,cACjC,CAIJ,EAyHEtD,8CACA8H,iBArQF,WACE,MAAMC,EAAoB7H,SAASC,cAAc,gBAAgBC,QAAQ,YAAYD,cAAc,iBAC/F4H,IACFA,EAAkB1H,YAAchB,EAAMI,qBAAuB,EAAI,UAAUJ,EAAMI,yBAA2B,QAEhH,EAkQF"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@archerjessop/utilities",
3
- "version": "7.11.0",
3
+ "version": "7.12.0",
4
4
  "description": "Shared utilities for ArcherJessop property analysis tools",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",