@archerjessop/utilities 7.0.0 → 7.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/browser/data/extractors.js.map +1 -0
- package/dist/browser/financial/tooltip-calculations.js +2 -0
- package/dist/browser/financial/tooltip-calculations.js.map +1 -0
- package/dist/browser/financial/tooltip-content-generators.js +2 -0
- package/dist/browser/financial/tooltip-content-generators.js.map +1 -0
- package/dist/browser/index.js +2 -0
- package/dist/browser/index.js.map +1 -0
- package/dist/browser/ui/click-handlers.js +2 -0
- package/dist/browser/ui/click-handlers.js.map +1 -0
- package/dist/browser/ui/tooltip-config.js +2 -0
- package/dist/browser/ui/tooltip-config.js.map +1 -0
- package/dist/browser/ui/tooltip-manager.js +2 -0
- package/dist/browser/ui/tooltip-manager.js.map +1 -0
- package/dist/browser/widget/createNavigationGuard.js +2 -0
- package/dist/browser/widget/createNavigationGuard.js.map +1 -0
- package/dist/browser/widget/createPanel.js +2 -0
- package/dist/browser/widget/createPanel.js.map +1 -0
- package/dist/export/export-logic.js +2 -0
- package/dist/export/export-logic.js.map +1 -0
- package/dist/financial/calculations.js +1 -1
- package/dist/financial/calculations.js.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/package.json +11 -5
- package/dist/data/extractors.js.map +0 -1
- /package/dist/{data → browser/data}/extractors.js +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"extractors.js","sources":["../../../src/browser/data/extractors.js"],"sourcesContent":["export function extractPhoneNumber() {\r\n const phoneElement = document.querySelector(\".phone-number\") ||\r\n document.querySelector(\"a[href^='tel:']\") ||\r\n document.querySelector(\".number\") ||\r\n document.querySelector(\"[class*='phone']\");\r\n\r\n if (phoneElement) {\r\n if (phoneElement.textContent && phoneElement.textContent.trim() !== \"Call\") {\r\n return phoneElement.textContent.trim();\r\n } else if (phoneElement.href) {\r\n // Extract from tel: link\r\n const telMatch = phoneElement.href.match(/tel:(.+)/);\r\n if (telMatch) {\r\n return telMatch[1];\r\n }\r\n }\r\n }\r\n\r\n // Fallback to text search with multiple patterns\r\n const pageText = document.body ? document.body.textContent || \"\" : \"\";\r\n const phoneMatch = pageText.match(/(\\+?1?\\s*\\(?[0-9]{3}\\)?[\\s.-]*[0-9]{3}[\\s.-]*[0-9]{4})/);\r\n if (phoneMatch) {\r\n return phoneMatch[1].trim();\r\n }\r\n\r\n return \"Not found\";\r\n}\r\n\r\nexport function extractBedrooms() {\r\n try {\r\n // Look for bedroom information in various places\r\n const bodyText = document.body?.textContent || \"\";\r\n\r\n // Common patterns for bedroom information\r\n const bedroomPatterns = [\r\n /(\\d+)\\s*bed/i,\r\n /(\\d+)\\s*bedroom/i,\r\n /beds?\\s*:\\s*(\\d+)/i,\r\n /bedrooms?\\s*:\\s*(\\d+)/i,\r\n /(\\d+)\\s*BR/i,\r\n /(\\d+)br/i\r\n ];\r\n\r\n for (const pattern of bedroomPatterns) {\r\n const match = bodyText.match(pattern);\r\n if (match) {\r\n const bedrooms = parseInt(match[1]);\r\n if (bedrooms > 0 && bedrooms < 100) { // Sanity check\r\n return bedrooms;\r\n }\r\n }\r\n }\r\n\r\n // Look in property details section specifically\r\n const propertyDetails = document.querySelector(\".property-details\") ||\r\n document.querySelector(\"#PropertyDetails\") ||\r\n document.querySelector(\".details\");\r\n\r\n if (propertyDetails) {\r\n const detailsText = propertyDetails.textContent || \"\";\r\n for (const pattern of bedroomPatterns) {\r\n const match = detailsText.match(pattern);\r\n if (match) {\r\n const bedrooms = parseInt(match[1]);\r\n if (bedrooms > 0 && bedrooms < 100) {\r\n return bedrooms;\r\n }\r\n }\r\n }\r\n }\r\n\r\n // Default fallback\r\n return 10; // Default assumption for assisted living\r\n } catch (error) {\r\n return 10; // Default fallback\r\n }\r\n}\r\n"],"names":["extractPhoneNumber","phoneElement","document","querySelector","textContent","trim","href","telMatch","match","phoneMatch","body","extractBedrooms","bodyText","bedroomPatterns","pattern","bedrooms","parseInt","propertyDetails","detailsText","error"],"mappings":"AAAO,SAASA,qBACd,MAAMC,EAAeC,SAASC,cAAc,kBACxBD,SAASC,cAAc,oBACvBD,SAASC,cAAc,YACvBD,SAASC,cAAc,oBAE3C,GAAIF,EAAc,CAChB,GAAIA,EAAaG,aAAmD,SAApCH,EAAaG,YAAYC,OACvD,OAAOJ,EAAaG,YAAYC,OAC3B,GAAIJ,EAAaK,KAAM,CAE5B,MAAMC,EAAWN,EAAaK,KAAKE,MAAM,YACzC,GAAID,EACF,OAAOA,EAAS,EAEpB,CACF,CAGA,MACME,GADWP,SAASQ,MAAOR,SAASQ,KAAKN,aAAoB,IACvCI,MAAM,0DAClC,OAAIC,EACKA,EAAW,GAAGJ,OAGhB,WACT,CAEO,SAASM,kBACd,IAEE,MAAMC,EAAWV,SAASQ,MAAMN,aAAe,GAGzCS,EAAkB,CACtB,eACA,mBACA,qBACA,yBACA,cACA,YAGF,IAAK,MAAMC,KAAWD,EAAiB,CACrC,MAAML,EAAQI,EAASJ,MAAMM,GAC7B,GAAIN,EAAO,CACT,MAAMO,EAAWC,SAASR,EAAM,IAChC,GAAIO,EAAW,GAAKA,EAAW,IAC7B,OAAOA,CAEX,CACF,CAGA,MAAME,EAAkBf,SAASC,cAAc,sBACxBD,SAASC,cAAc,qBACvBD,SAASC,cAAc,YAE9C,GAAIc,EAAiB,CACnB,MAAMC,EAAcD,EAAgBb,aAAe,GACnD,IAAK,MAAMU,KAAWD,EAAiB,CACrC,MAAML,EAAQU,EAAYV,MAAMM,GAChC,GAAIN,EAAO,CACT,MAAMO,EAAWC,SAASR,EAAM,IAChC,GAAIO,EAAW,GAAKA,EAAW,IAC7B,OAAOA,CAEX,CACF,CACF,CAGA,OAAO,EACT,CAAE,MAAOI,GACP,OAAO,EACT,CACF"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import{calculateCashFlowYield as a,calculatePMT as t}from"../../financial/calculations.js";import{formatCurrency as n,formatPercentage as e}from"../../financial/formatters.js";import{FINANCIAL_CONSTANTS as l}from"../../config/financial.js";function calculateDownPaymentTooltip(a,n,o,r,c,i="dscr_residential"){try{const r=l.INTEREST_RATE_TIERS[i||"dscr_residential"],c=a*(o/100),s=100-o,p=Math.min(s,70),u=Math.max(0,s-70),h=a*(u/100),m=12*t(a*(p/100),r.rate,r.amortization),F=h>0?12*t(h,l.SELLER_FI_INTEREST_RATE,l.SELLER_FI_AMORTIZATION):0;return`COCR at ${o}% down (${u>0?`${p}% DSCR + ${u}% Seller FI`:`${p}% DSCR only`}): ${e((n-m-F)/c*100)}`}catch(a){return`Down payment at ${o}%`}}function calculateCashFlowTooltip(t,e){try{const l=n(12*e);return`Cash Flow Yield: ${a(e,t).toFixed(1)}% (${l}/yr)`}catch(a){return`Monthly cash flow: ${n(e,!0)} (${n(12*e)}/yr)`}}function parseFinancialData(a,t){const n=a.match(/[\d,]+/),e=t.match(/[\d,.]+/);if(n&&e){const a=parseFloat(n[0].replace(/,/g,""));let l=parseFloat(e[0].replace(/,/g,""));return t.includes("K")?l*=1e3:t.includes("M")&&(l*=1e6),{price:a,noi:l}}return null}function parseCashFlowData(a,t){const n=a.match(/[\d,]+/),e=t.match(/-?[\d,]+/);if(n&&e){const a=parseFloat(n[0].replace(/,/g,""));let l=parseFloat(e[0].replace(/,/g,""));return t.includes("-")&&(l=-Math.abs(l)),{price:a,monthlyCashFlow:l}}return null}export{calculateCashFlowTooltip,calculateDownPaymentTooltip,parseCashFlowData,parseFinancialData};
|
|
2
|
+
//# sourceMappingURL=tooltip-calculations.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tooltip-calculations.js","sources":["../../../src/browser/financial/tooltip-calculations.js"],"sourcesContent":["import { calculatePMT, calculateCashFlowYield } from '../../financial/calculations.js';\r\nimport { formatPercentage, formatCurrency } from '../../financial/formatters.js';\r\nimport { FINANCIAL_CONSTANTS } from '../../config/financial.js';\r\n\r\nexport function calculateDownPaymentTooltip(price, noi, downPercent, dscrPercent, sellerFiPercent, interestRateType = \"dscr_residential\") {\r\n try {\r\n const tier = FINANCIAL_CONSTANTS.INTEREST_RATE_TIERS[interestRateType || \"dscr_residential\"];\r\n const downDecimal = downPercent / 100;\r\n const cashInvested = price * downDecimal;\r\n\r\n const remainingPercent = 100 - downPercent;\r\n const dscrLoanPercent = Math.min(remainingPercent, 70);\r\n const actualSellerFiPercent = Math.max(0, remainingPercent - 70);\r\n\r\n const dscrLoanAmount = price * (dscrLoanPercent / 100);\r\n const sellerFiAmount = price * (actualSellerFiPercent / 100);\r\n\r\n const dscrPayment = calculatePMT(dscrLoanAmount, tier.rate, tier.amortization) * 12;\r\n const sellerFiPayment = sellerFiAmount > 0 ?\r\n calculatePMT(sellerFiAmount, FINANCIAL_CONSTANTS.SELLER_FI_INTEREST_RATE, FINANCIAL_CONSTANTS.SELLER_FI_AMORTIZATION) * 12 : 0;\r\n\r\n const annualCashFlow = noi - dscrPayment - sellerFiPayment;\r\n const cocr = (annualCashFlow / cashInvested) * 100;\r\n\r\n let financingType = actualSellerFiPercent > 0 ?\r\n `${dscrLoanPercent}% DSCR + ${actualSellerFiPercent}% Seller FI` :\r\n `${dscrLoanPercent}% DSCR only`;\r\n\r\n return `COCR at ${downPercent}% down (${financingType}): ${formatPercentage(cocr)}`;\r\n } catch (error) {\r\n return `Down payment at ${downPercent}%`;\r\n }\r\n}\r\n\r\nexport function calculateCashFlowTooltip(price, monthlyCashFlow) {\r\n try {\r\n const annualCashFlow = formatCurrency(monthlyCashFlow * 12);\r\n const cashFlowYield = calculateCashFlowYield(monthlyCashFlow, price);\r\n return `Cash Flow Yield: ${cashFlowYield.toFixed(1)}% (${annualCashFlow}/yr)`;\r\n } catch (error) {\r\n return `Monthly cash flow: ${formatCurrency(monthlyCashFlow, true)} (${formatCurrency(monthlyCashFlow * 12)}/yr)`;\r\n }\r\n}\r\n\r\nexport function parseFinancialData(priceText, noiText) {\r\n const priceMatch = priceText.match(/[\\d,]+/);\r\n const noiMatch = noiText.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 (noiText.includes(\"K\")) {\r\n noi *= 1000;\r\n } else if (noiText.includes(\"M\")) {\r\n noi *= 1000000;\r\n }\r\n\r\n return { price, noi };\r\n }\r\n\r\n return null;\r\n}\r\n\r\nexport function parseCashFlowData(priceText, cashFlowText) {\r\n const priceMatch = priceText.match(/[\\d,]+/);\r\n const cashFlowMatch = cashFlowText.match(/-?[\\d,]+/);\r\n\r\n if (priceMatch && cashFlowMatch) {\r\n const price = parseFloat(priceMatch[0].replace(/,/g, \"\"));\r\n let monthlyCashFlow = parseFloat(cashFlowMatch[0].replace(/,/g, \"\"));\r\n\r\n if (cashFlowText.includes(\"-\")) {\r\n monthlyCashFlow = -Math.abs(monthlyCashFlow);\r\n }\r\n\r\n return { price, monthlyCashFlow };\r\n }\r\n\r\n return null;\r\n}\r\n"],"names":["calculateDownPaymentTooltip","price","noi","downPercent","dscrPercent","sellerFiPercent","interestRateType","tier","FINANCIAL_CONSTANTS","INTEREST_RATE_TIERS","cashInvested","remainingPercent","dscrLoanPercent","Math","min","actualSellerFiPercent","max","sellerFiAmount","dscrPayment","calculatePMT","rate","amortization","sellerFiPayment","SELLER_FI_INTEREST_RATE","SELLER_FI_AMORTIZATION","formatPercentage","error","calculateCashFlowTooltip","monthlyCashFlow","annualCashFlow","formatCurrency","calculateCashFlowYield","toFixed","parseFinancialData","priceText","noiText","priceMatch","match","noiMatch","parseFloat","replace","includes","parseCashFlowData","cashFlowText","cashFlowMatch","abs"],"mappings":"gPAIO,SAASA,4BAA4BC,EAAOC,EAAKC,EAAaC,EAAaC,EAAiBC,EAAmB,oBACpH,IACE,MAAMC,EAAOC,EAAoBC,oBAAoBH,GAAoB,oBAEnEI,EAAeT,GADDE,EAAc,KAG5BQ,EAAmB,IAAMR,EACzBS,EAAkBC,KAAKC,IAAIH,EAAkB,IAC7CI,EAAwBF,KAAKG,IAAI,EAAGL,EAAmB,IAGvDM,EAAiBhB,GAASc,EAAwB,KAElDG,EAA2E,GAA7DC,EAHGlB,GAASW,EAAkB,KAGDL,EAAKa,KAAMb,EAAKc,cAC3DC,EAAkBL,EAAiB,EACiF,GAAxHE,EAAaF,EAAgBT,EAAoBe,wBAAyBf,EAAoBgB,wBAA+B,EAS/H,MAAO,WAAWrB,YAJEY,EAAwB,EAC1C,GAAGH,aAA2BG,eAC9B,GAAGH,oBAEsDa,GAPpCvB,EAAMgB,EAAcI,GACZZ,EAAgB,MAOjD,CAAE,MAAOgB,GACP,MAAO,mBAAmBvB,IAC5B,CACF,CAEO,SAASwB,yBAAyB1B,EAAO2B,GAC9C,IACE,MAAMC,EAAiBC,EAAiC,GAAlBF,GAEtC,MAAO,oBADeG,EAAuBH,EAAiB3B,GACrB+B,QAAQ,QAAQH,OAC3D,CAAE,MAAOH,GACP,MAAO,sBAAsBI,EAAeF,GAAiB,OAAUE,EAAiC,GAAlBF,QACxF,CACF,CAEO,SAASK,mBAAmBC,EAAWC,GAC5C,MAAMC,EAAaF,EAAUG,MAAM,UAC7BC,EAAWH,EAAQE,MAAM,WAE/B,GAAID,GAAcE,EAAU,CAC1B,MAAMrC,EAAQsC,WAAWH,EAAW,GAAGI,QAAQ,KAAM,KACrD,IAAItC,EAAMqC,WAAWD,EAAS,GAAGE,QAAQ,KAAM,KAQ/C,OANIL,EAAQM,SAAS,KACnBvC,GAAO,IACEiC,EAAQM,SAAS,OAC1BvC,GAAO,KAGF,CAAED,QAAOC,MAClB,CAEA,OAAO,IACT,CAEO,SAASwC,kBAAkBR,EAAWS,GAC3C,MAAMP,EAAaF,EAAUG,MAAM,UAC7BO,EAAgBD,EAAaN,MAAM,YAEzC,GAAID,GAAcQ,EAAe,CAC/B,MAAM3C,EAAQsC,WAAWH,EAAW,GAAGI,QAAQ,KAAM,KACrD,IAAIZ,EAAkBW,WAAWK,EAAc,GAAGJ,QAAQ,KAAM,KAMhE,OAJIG,EAAaF,SAAS,OACxBb,GAAmBf,KAAKgC,IAAIjB,IAGvB,CAAE3B,QAAO2B,kBAClB,CAEA,OAAO,IACT"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import{calculateDownPaymentTooltip as e}from"./tooltip-calculations.js";function generatePriceTooltipHTML(e){return`\n <strong>Click the price to decrease by 10%</strong><br>\n Current discount: ${e}%\n <hr>\n <em>Click the label to reset</em>\n `}function generateCashFlowTooltipHTML(e,t){const n=12*t;return`\n <strong>Cash Flow Yield:</strong> ${(n/e*100).toFixed(1)}%<br>\n <strong>Annual Cash Flow:</strong> $${n.toLocaleString()}\n `}function generateCapRateTooltipHTML(e){return e?"\n <strong>Click the cap rate to increase by 1%</strong>\n <hr>\n <em>Click the label to reset</em>\n ":null}function generateDownPaymentTooltipHTML(t,n,o,r,l,a="dscr_residential"){return`\n <strong>Click the down payment to decrease by 10%</strong><br>\n ${e(t,n,o,r,l,a)}\n <hr>\n <em>Click the label to reset</em>\n `}export{generateCapRateTooltipHTML,generateCashFlowTooltipHTML,generateDownPaymentTooltipHTML,generatePriceTooltipHTML};
|
|
2
|
+
//# sourceMappingURL=tooltip-content-generators.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tooltip-content-generators.js","sources":["../../../src/browser/financial/tooltip-content-generators.js"],"sourcesContent":["import { calculateDownPaymentTooltip, calculateCashFlowTooltip } from './tooltip-calculations.js';\r\n\r\nexport function generatePriceTooltipHTML(priceDiscount) {\r\n return `\r\n <strong>Click the price to decrease by 10%</strong><br>\r\n Current discount: ${priceDiscount}%\r\n <hr>\r\n <em>Click the label to reset</em>\r\n `;\r\n}\r\n\r\nexport function generateCashFlowTooltipHTML(price, monthlyCashFlow) {\r\n const annualCashFlow = monthlyCashFlow * 12;\r\n const cashFlowYield = ((annualCashFlow / price) * 100).toFixed(1);\r\n\r\n return `\r\n <strong>Cash Flow Yield:</strong> ${cashFlowYield}%<br>\r\n <strong>Annual Cash Flow:</strong> $${annualCashFlow.toLocaleString()}\r\n `;\r\n}\r\n\r\nexport function generateCapRateTooltipHTML(isUsingEstimatedCapRate) {\r\n if (!isUsingEstimatedCapRate) return null;\r\n\r\n return `\r\n <strong>Click the cap rate to increase by 1%</strong>\r\n <hr>\r\n <em>Click the label to reset</em>\r\n `;\r\n}\r\n\r\nexport function generateDownPaymentTooltipHTML(price, noi, downPercent, dscrPercent, sellerFiPercent, interestRateType = \"dscr_residential\") {\r\n // Use existing calculation function\r\n const cocrText = calculateDownPaymentTooltip(price, noi, downPercent, dscrPercent, sellerFiPercent, interestRateType);\r\n\r\n return `\r\n <strong>Click the down payment to decrease by 10%</strong><br>\r\n ${cocrText}\r\n <hr>\r\n <em>Click the label to reset</em>\r\n `;\r\n}\r\n"],"names":["generatePriceTooltipHTML","priceDiscount","generateCashFlowTooltipHTML","price","monthlyCashFlow","annualCashFlow","toFixed","toLocaleString","generateCapRateTooltipHTML","isUsingEstimatedCapRate","generateDownPaymentTooltipHTML","noi","downPercent","dscrPercent","sellerFiPercent","interestRateType","calculateDownPaymentTooltip"],"mappings":"wEAEO,SAASA,yBAAyBC,GACvC,MAAO,wFAEeA,yDAIxB,CAEO,SAASC,4BAA4BC,EAAOC,GACjD,MAAMC,EAAmC,GAAlBD,EAGvB,MAAO,4CAFiBC,EAAiBF,EAAS,KAAKG,QAAQ,oDAIvBD,EAAeE,sBAEzD,CAEO,SAASC,2BAA2BC,GACzC,OAAKA,EAEE,mHAF8B,IAOvC,CAEO,SAASC,+BAA+BP,EAAOQ,EAAKC,EAAaC,EAAaC,EAAiBC,EAAmB,oBAIvH,MAAO,6EAFUC,EAA4Bb,EAAOQ,EAAKC,EAAaC,EAAaC,EAAiBC,yDAQtG"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
export{extractBedrooms,extractPhoneNumber}from"./data/extractors.js";export{calculateCashFlowTooltip,calculateDownPaymentTooltip,parseCashFlowData,parseFinancialData}from"./financial/tooltip-calculations.js";export{generateCapRateTooltipHTML,generateCashFlowTooltipHTML,generateDownPaymentTooltipHTML,generatePriceTooltipHTML}from"./financial/tooltip-content-generators.js";export{setupCapRateClickHandler,setupDiscountButtonHandler,setupDownPaymentClickHandler,setupPriceClickHandler}from"./ui/click-handlers.js";export{CLICKABLE_TOOLTIPS,TOOLTIP_ENABLED_METRICS}from"./ui/tooltip-config.js";export{attachTooltip,hasTooltip,isTooltipVisible,removeAllTooltips,removeTooltip,updateTooltipContent}from"./ui/tooltip-manager.js";export{createNavigationGuard}from"./widget/createNavigationGuard.js";export{createPanel}from"./widget/createPanel.js";
|
|
2
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sources":[],"sourcesContent":[],"names":[],"mappings":""}
|
|
@@ -0,0 +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}),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}),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};
|
|
2
|
+
//# sourceMappingURL=click-handlers.js.map
|
|
@@ -0,0 +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 });\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 });\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","generateCapRateTooltipHTML","originalCapRate","originalEstimatedCapRate","FINANCIAL_CONSTANTS","DEFAULT_CAP_RATE","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,IAEvCH,EAAWnC,YAAc,GAAGsC,MAC5BlC,EAAUW,wBAENG,EAAQ,CACV,MAAMM,EAAiBgB,EAA2B5C,EAAMyC,yBACpDb,GACFE,EAAqBR,EAAQM,EAEjC,CACF,GAEAY,EAAgB5B,iBAAiB,QAAS,SAASC,GACjDA,EAAEC,iBACFD,EAAEE,kBAEF,MAAM8B,EAAkB7C,EAAM8C,0BAA4BC,EAAoBC,iBAM9E,GALArC,EAAY,CAAEgC,wBAAyBE,IAEvCN,EAAWnC,YAAc,GAAGyC,MAC5BrC,EAAUW,wBAENG,EAAQ,CACV,MAAMM,EAAiBgB,EAA2B5C,EAAMyC,yBACpDb,GACFE,EAAqBR,EAAQM,EAEjC,CACF,GAEIN,EAAQ,CACV,MAAMM,EAAiBgB,EAA2B5C,EAAMyC,yBACpDb,IACFK,EAAcX,EAAQM,GACtBY,EAAgBN,UAAUC,IAAI,eAElC,CAEAI,EAAWH,MAAMC,OAAS,UAC1BG,EAAgBJ,MAAMC,OAAS,SACjC,CAEO,SAASY,6BAA6BC,EAAaC,EAAkB3C,GAC1E,IAAK0C,IAAgBC,EAAkB,OAGvC,GAA4C,SAAxCD,EAAYzC,QAAQC,gBAA4B,OACpDwC,EAAYzC,QAAQC,gBAAkB,OAEtC,MAAMV,MAAEA,EAAKW,YAAEA,GAAgBH,EACzBc,EAAS4B,EAAY3B,QAAQ,WAEnC2B,EAAYtC,iBAAiB,QAAS,SAASC,GAC7CA,EAAEC,iBACFD,EAAEE,kBAEF,IAAIqC,EAAiBpD,EAAMqD,0BAA4B,GACnDC,EAAiBtD,EAAMuD,mBAAqB,GAC5CC,EAAqBxD,EAAMyD,uBAAyB,GAEpDL,EAAiB,IACnBA,EAAiB,GACjBE,EAAiB,GACjBE,EAAqB,IAGvB7C,EAAY,CACV0C,0BAA2BD,EAC3BG,mBAAoBD,EACpBG,uBAAwBD,IAG1BhD,EAAUkD,yBACVlD,EAAUW,wBAEVwC,WAAW,KACT,MAAM3C,EAAed,SAASC,eAAe,cACvCyD,EAAa1D,SAASC,eAAe,YAE3C,GAAIa,GAAgB4C,GAActC,EAAQ,CACxC,MAAMuC,EAAa7C,EAAaZ,YAAY0D,MAAM,UAC5CC,EAAWH,EAAWxD,YAAY0D,MAAM,WAE9C,GAAID,GAAcE,EAAU,CAC1B,MAAMC,EAAQC,WAAWJ,EAAW,GAAGK,QAAQ,KAAM,KACrD,IAAIC,EAAMF,WAAWF,EAAS,GAAGG,QAAQ,KAAM,KAE3CN,EAAWxD,YAAYgE,SAAS,OAAMD,GAAO,KAC7CP,EAAWxD,YAAYgE,SAAS,OAAMD,GAAO,KAEjDE,EAAc/C,GACdqC,WAAW,KACT,MAAM/B,EAAiB0C,EACrBN,EACAG,EACAnE,EAAMqD,0BACNrD,EAAMuD,mBACNvD,EAAMyD,uBACNzD,EAAMuE,yBAERzC,EAAqBR,EAAQM,IAC5B,GACL,CACF,GACC,IACL,GAEAuB,EAAiBvC,iBAAiB,QAAS,SAASC,GAClDA,EAAEC,iBACFD,EAAEE,kBAEFJ,EAAY,CACV0C,0BAAwE,IAA7CN,EAAoByB,uBAC/CjB,mBAAkE,IAA9CR,EAAoB0B,wBACxChB,uBAA8D,IAAtCV,EAAoB2B,kBAG9ClE,EAAUkD,yBACVlD,EAAUW,wBAEVwC,WAAW,KACT,MAAM3C,EAAed,SAASC,eAAe,cACvCyD,EAAa1D,SAASC,eAAe,YAE3C,GAAIa,GAAgB4C,GAActC,EAAQ,CACxC,MAAMuC,EAAa7C,EAAaZ,YAAY0D,MAAM,UAC5CC,EAAWH,EAAWxD,YAAY0D,MAAM,WAE9C,GAAID,GAAcE,EAAU,CAC1B,MAAMC,EAAQC,WAAWJ,EAAW,GAAGK,QAAQ,KAAM,KACrD,IAAIC,EAAMF,WAAWF,EAAS,GAAGG,QAAQ,KAAM,KAE3CN,EAAWxD,YAAYgE,SAAS,OAAMD,GAAO,KAC7CP,EAAWxD,YAAYgE,SAAS,OAAMD,GAAO,KAEjDE,EAAc/C,GACdqC,WAAW,KACT,MAAM/B,EAAiB0C,EACrBN,EACAG,EACAnE,EAAMqD,0BACNrD,EAAMuD,mBACNvD,EAAMyD,uBACNzD,EAAMuE,yBAERzC,EAAqBR,EAAQM,IAC5B,GACL,CACF,GACC,IACL,GAEIN,GAAU6B,GACZA,EAAiBjB,UAAUC,IAAI,eAGjCe,EAAYd,MAAMC,OAAS,UAC3Bc,EAAiBf,MAAMC,OAAS,SAClC"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
const a={"prop-down":{dynamic:!0,calculate:"calculateDownPaymentTooltip"},"prop-cashflow":{dynamic:!0,calculate:"calculateCashFlowTooltip"},"prop-cap":{dynamic:!0,conditional:!0,calculate:"calculateCapRateTooltip"}},c={"prop-price":"Click to adjust asking price discount","prop-cap":"Click to adjust cap rate","prop-down":"Click to decrease down payment by 10%"};export{c as CLICKABLE_TOOLTIPS,a as TOOLTIP_ENABLED_METRICS};
|
|
2
|
+
//# sourceMappingURL=tooltip-config.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tooltip-config.js","sources":["../../../src/browser/ui/tooltip-config.js"],"sourcesContent":["// Metrics that should have tooltips\r\nexport const TOOLTIP_ENABLED_METRICS = {\r\n \"prop-down\": {\r\n dynamic: true, // Content updates based on calculations\r\n calculate: \"calculateDownPaymentTooltip\"\r\n },\r\n \"prop-cashflow\": {\r\n dynamic: true,\r\n calculate: \"calculateCashFlowTooltip\"\r\n },\r\n \"prop-cap\": {\r\n dynamic: true,\r\n conditional: true, // Only shows when price is discounted\r\n calculate: \"calculateCapRateTooltip\"\r\n }\r\n // Add more as needed\r\n};\r\n\r\n// Static tooltips for clickable elements\r\nexport const CLICKABLE_TOOLTIPS = {\r\n \"prop-price\": \"Click to adjust asking price discount\",\r\n \"prop-cap\": \"Click to adjust cap rate\",\r\n \"prop-down\": \"Click to decrease down payment by 10%\"\r\n};\r\n"],"names":["TOOLTIP_ENABLED_METRICS","dynamic","calculate","conditional","CLICKABLE_TOOLTIPS"],"mappings":"AACY,MAACA,EAA0B,CACrC,YAAa,CACXC,SAAS,EACTC,UAAW,+BAEb,gBAAiB,CACfD,SAAS,EACTC,UAAW,4BAEb,WAAY,CACVD,SAAS,EACTE,aAAa,EACbD,UAAW,4BAMFE,EAAqB,CAChC,aAAc,wCACd,WAAY,2BACZ,YAAa"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import{computePosition as t,offset as o,flip as e,shift as i,arrow as l}from"@floating-ui/dom";const n=new Map;function attachTooltip(s,p,r={}){if(!s||!p)return;const a=`tooltip-${Math.random().toString(36).substr(2,9)}`,d=document.createElement("div");d.className="floating-tooltip",d.id=a,d.innerHTML=p,d.setAttribute("role","tooltip");const c=document.createElement("div");c.className="floating-tooltip-arrow",d.appendChild(c),document.body.appendChild(d);const m=r.placement||"top";async function updatePosition(){const{x:n,y:p,placement:r,middlewareData:a}=await t(s,d,{placement:m,middleware:[o(8),e(),i({padding:5}),l({element:c})]});if(Object.assign(d.style,{left:`${n}px`,top:`${p}px`}),a.arrow){const{x:t,y:o}=a.arrow,e={top:"bottom",right:"left",bottom:"top",left:"right"}[r.split("-")[0]];Object.assign(c.style,{left:null!=t?`${t}px`:"",top:null!=o?`${o}px`:"",right:"",bottom:"",[e]:"-4px"})}}function showTooltip(){d.style.display="block",updatePosition()}function hideTooltip(){d.style.display=""}return s.addEventListener("mouseenter",showTooltip),s.addEventListener("mouseleave",hideTooltip),s.addEventListener("focus",showTooltip),s.addEventListener("blur",hideTooltip),n.set(s,{tooltipElement:d,arrowElement:c,showTooltip:showTooltip,hideTooltip:hideTooltip,updatePosition:updatePosition,cleanup:()=>{s.removeEventListener("mouseenter",showTooltip),s.removeEventListener("mouseleave",hideTooltip),s.removeEventListener("focus",showTooltip),s.removeEventListener("blur",hideTooltip),d.remove(),n.delete(s)}}),a}function updateTooltipContent(t,o){const e=n.get(t);if(!e)return;const i="block"===e.tooltipElement.style.display;Array.from(e.tooltipElement.childNodes).forEach(t=>{t!==e.arrowElement&&t.remove()});const l=document.createElement("div");for(l.innerHTML=o;l.firstChild;)e.tooltipElement.insertBefore(l.firstChild,e.arrowElement);i&&(e.tooltipElement.style.display="block",e.updatePosition())}function removeTooltip(t){const o=n.get(t);o&&o.cleanup()}function hasTooltip(t){return n.has(t)}function removeAllTooltips(){n.forEach(t=>t.cleanup()),n.clear()}function isTooltipVisible(t){const o=n.get(t);return!!o&&"block"===o.tooltipElement.style.display}export{attachTooltip,hasTooltip,isTooltipVisible,removeAllTooltips,removeTooltip,updateTooltipContent};
|
|
2
|
+
//# sourceMappingURL=tooltip-manager.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tooltip-manager.js","sources":["../../../src/browser/ui/tooltip-manager.js"],"sourcesContent":["import { computePosition, flip, shift, offset, arrow } from \"@floating-ui/dom\";\r\n\r\nconst tooltips = new Map();\r\n\r\n/**\r\n * Creates and attaches a Floating UI tooltip to an element\r\n * @param {HTMLElement} element - The element to attach tooltip to\r\n * @param {string} content - The tooltip content\r\n * @param {Object} options - Configuration options\r\n */\r\nexport function attachTooltip(element, content, options = {}) {\r\n if (!element || !content) return;\r\n\r\n const tooltipId = `tooltip-${Math.random().toString(36).substr(2, 9)}`;\r\n\r\n const tooltip = document.createElement(\"div\");\r\n tooltip.className = \"floating-tooltip\";\r\n tooltip.id = tooltipId;\r\n tooltip.innerHTML = content;\r\n tooltip.setAttribute(\"role\", \"tooltip\");\r\n\r\n const arrowElement = document.createElement(\"div\");\r\n arrowElement.className = \"floating-tooltip-arrow\";\r\n tooltip.appendChild(arrowElement);\r\n\r\n document.body.appendChild(tooltip);\r\n\r\n const placement = options.placement || \"top\";\r\n\r\n async function updatePosition() {\r\n const { x, y, placement: finalPlacement, middlewareData } = await computePosition(element, tooltip, {\r\n placement,\r\n middleware: [\r\n offset(8),\r\n flip(),\r\n shift({ padding: 5 }),\r\n arrow({ element: arrowElement })\r\n ]\r\n });\r\n\r\n Object.assign(tooltip.style, {\r\n left: `${x}px`,\r\n top: `${y}px`\r\n });\r\n\r\n if (middlewareData.arrow) {\r\n const { x: arrowX, y: arrowY } = middlewareData.arrow;\r\n const staticSide = {\r\n top: \"bottom\",\r\n right: \"left\",\r\n bottom: \"top\",\r\n left: \"right\"\r\n }[finalPlacement.split(\"-\")[0]];\r\n\r\n Object.assign(arrowElement.style, {\r\n left: arrowX != null ? `${arrowX}px` : \"\",\r\n top: arrowY != null ? `${arrowY}px` : \"\",\r\n right: \"\",\r\n bottom: \"\",\r\n [staticSide]: \"-4px\"\r\n });\r\n }\r\n }\r\n\r\n function showTooltip() {\r\n tooltip.style.display = \"block\";\r\n updatePosition();\r\n }\r\n\r\n function hideTooltip() {\r\n tooltip.style.display = \"\";\r\n }\r\n\r\n element.addEventListener(\"mouseenter\", showTooltip);\r\n element.addEventListener(\"mouseleave\", hideTooltip);\r\n element.addEventListener(\"focus\", showTooltip);\r\n element.addEventListener(\"blur\", hideTooltip);\r\n\r\n tooltips.set(element, {\r\n tooltipElement: tooltip,\r\n arrowElement,\r\n showTooltip,\r\n hideTooltip,\r\n updatePosition,\r\n cleanup: () => {\r\n element.removeEventListener(\"mouseenter\", showTooltip);\r\n element.removeEventListener(\"mouseleave\", hideTooltip);\r\n element.removeEventListener(\"focus\", showTooltip);\r\n element.removeEventListener(\"blur\", hideTooltip);\r\n tooltip.remove();\r\n tooltips.delete(element);\r\n }\r\n });\r\n\r\n return tooltipId;\r\n}\r\n\r\n/**\r\n * Updates the content of an existing tooltip\r\n * @param {HTMLElement} element - The element with the tooltip\r\n * @param {string} newContent - The new tooltip content\r\n */\r\nexport function updateTooltipContent(element, newContent) {\r\n const tooltipData = tooltips.get(element);\r\n if (!tooltipData) return;\r\n\r\n const wasVisible = tooltipData.tooltipElement.style.display === 'block';\r\n\r\n // Clear existing content (except arrow)\r\n Array.from(tooltipData.tooltipElement.childNodes).forEach(node => {\r\n if (node !== tooltipData.arrowElement) {\r\n node.remove();\r\n }\r\n });\r\n\r\n // Insert new HTML content before arrow\r\n const temp = document.createElement('div');\r\n temp.innerHTML = newContent;\r\n\r\n while (temp.firstChild) {\r\n tooltipData.tooltipElement.insertBefore(temp.firstChild, tooltipData.arrowElement);\r\n }\r\n\r\n // Restore visibility and update position\r\n if (wasVisible) {\r\n tooltipData.tooltipElement.style.display = 'block';\r\n tooltipData.updatePosition();\r\n }\r\n}\r\n\r\n/**\r\n * Removes a tooltip from an element\r\n * @param {HTMLElement} element - The element to remove tooltip from\r\n */\r\nexport function removeTooltip(element) {\r\n const tooltipData = tooltips.get(element);\r\n if (tooltipData) {\r\n tooltipData.cleanup();\r\n }\r\n}\r\n\r\n/**\r\n * Checks if an element has a tooltip attached\r\n * @param {HTMLElement} element - The element to check\r\n * @returns {boolean}\r\n */\r\nexport function hasTooltip(element) {\r\n return tooltips.has(element);\r\n}\r\n\r\n/**\r\n * Removes all tooltips\r\n */\r\nexport function removeAllTooltips() {\r\n tooltips.forEach((data) => data.cleanup());\r\n tooltips.clear();\r\n}\r\n\r\n/**\r\n * Check if tooltip is currently visible\r\n */\r\nexport function isTooltipVisible(element) {\r\n const tooltipData = tooltips.get(element);\r\n if (!tooltipData) return false;\r\n return tooltipData.tooltipElement.style.display === 'block';\r\n}\r\n"],"names":["tooltips","Map","attachTooltip","element","content","options","tooltipId","Math","random","toString","substr","tooltip","document","createElement","className","id","innerHTML","setAttribute","arrowElement","appendChild","body","placement","async","updatePosition","x","y","finalPlacement","middlewareData","computePosition","middleware","offset","flip","shift","padding","arrow","Object","assign","style","left","top","arrowX","arrowY","staticSide","right","bottom","split","showTooltip","display","hideTooltip","addEventListener","set","tooltipElement","cleanup","removeEventListener","remove","delete","updateTooltipContent","newContent","tooltipData","get","wasVisible","Array","from","childNodes","forEach","node","temp","firstChild","insertBefore","removeTooltip","hasTooltip","has","removeAllTooltips","data","clear","isTooltipVisible"],"mappings":"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"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
function createNavigationGuard(e){if("function"!=typeof e)throw new TypeError("createNavigationGuard requires a getCurrentId function");let r,t=!1;return{capture:()=>(r=e(),t=!0,r),isStale:()=>!!t&&e()!==r}}export{createNavigationGuard};
|
|
2
|
+
//# sourceMappingURL=createNavigationGuard.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"createNavigationGuard.js","sources":["../../../src/browser/widget/createNavigationGuard.js"],"sourcesContent":["/**\r\n * Navigation guard for in-flight async work on SPA platforms.\r\n *\r\n * On a single-page-app target (e.g. Zillow) the user can navigate from listing A\r\n * to listing B before a slow request (equity, STR revenue) resolves. Without a\r\n * guard, A's late response gets cached onto B and the panel silently shows the\r\n * wrong numbers. Capture an identity at request start; drop the response if the\r\n * current identity no longer matches.\r\n *\r\n * Multi-page-app targets (e.g. LoopNet, full reload) pass a getCurrentId whose\r\n * value is stable for the page's life — isStale() is then always false, so the\r\n * shared async services behave exactly as before with no per-platform branching.\r\n *\r\n * @param {() => (string|number|null|undefined)} getCurrentId - returns the\r\n * current listing identity (e.g. the zpid parsed from the URL)\r\n * @returns {{ capture: () => (string|number|null|undefined), isStale: () => boolean }}\r\n */\r\nexport function createNavigationGuard(getCurrentId) {\r\n if (typeof getCurrentId !== \"function\") {\r\n throw new TypeError(\"createNavigationGuard requires a getCurrentId function\");\r\n }\r\n\r\n let capturedId;\r\n let captured = false;\r\n\r\n return {\r\n /**\r\n * Snapshot the current identity. Call this at the start of an async request.\r\n * @returns the captured identity\r\n */\r\n capture() {\r\n capturedId = getCurrentId();\r\n captured = true;\r\n return capturedId;\r\n },\r\n\r\n /**\r\n * True if the current identity differs from the captured one (the user\r\n * navigated away). Returns false if capture() was never called.\r\n * @returns {boolean}\r\n */\r\n isStale() {\r\n if (!captured) return false;\r\n return getCurrentId() !== capturedId;\r\n }\r\n };\r\n}\r\n"],"names":["createNavigationGuard","getCurrentId","TypeError","capturedId","captured","capture","isStale"],"mappings":"AAiBO,SAASA,sBAAsBC,GACpC,GAA4B,mBAAjBA,EACT,MAAM,IAAIC,UAAU,0DAGtB,IAAIC,EACAC,GAAW,EAEf,MAAO,CAKLC,QAAO,KACLF,EAAaF,IACbG,GAAW,EACJD,GAQTG,QAAO,MACAF,GACEH,MAAmBE,EAGhC"}
|
|
@@ -0,0 +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};
|
|
2
|
+
//# sourceMappingURL=createPanel.js.map
|
|
@@ -0,0 +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"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
function createExportObjectCore(e,t={}){const{cachedEquity:n=null,currentDownPaymentPercent:r,currentInterestRateType:a="dscr_residential",currentPriceDiscount:o=0,currentPropertyType:c="str",equitySource:i="scraped",isUsingEstimatedCapRate:s=!1,numberOfUnits:u=4,priceWasDefaulted:d=!1,windowLocation:p=""}=t;if(d)return null;const l={};if(e.name&&"Property Details"!==e.name&&"Not found"!==e.name&&(l.address=e.name),e.capRate&&"Loading..."!==e.capRate&&"Not found"!==e.capRate){const t=e.capRate.match(/[\d.]+/);if(t){const n=parseFloat(t[0]);e.capRate.includes("%")||n>1?l.capRate=Math.round(n/100*1e6)/1e6:l.capRate=Math.round(1e6*n)/1e6}}if(l.capRateSource=s?"estimated":"scraped",e.contact&&"Not found"!==e.contact&&(l.contact=e.contact),e.listingDate&&"Not found"!==e.listingDate&&(l.dateListed=e.listingDate),e.price&&"Loading..."!==e.price&&"Not found"!==e.price){const t=e.price.match(/[\d,]+/);if(t){const e=parseFloat(t[0].replace(/,/g,""));if(o>0){const t=e/(1-o/100);l.price=Math.round(t)}else l.price=e}}if(void 0!==r&&(l.downPaymentPercent=Math.round(r/100*1e6)/1e6),n&&"Loading..."!==n){const e=n.match(/[\d.]+/);e&&(l.equityPercent=Math.round(parseFloat(e[0])/100*1e6)/1e6)}l.equitySource=i,l.numberOfUnits=u,e.phone&&"Not found"!==e.phone&&(l.phone=e.phone),l.priceDiscountPercent=o>0?Math.round(o/100*1e6)/1e6:0,l.interestRateType=a,l.propertyType={assisted:"assisted",business:"business",mixed_use:"mixed_use",multifamily:"mfr",rv_park:"rv_park",str:"str"}[c]||"mfr",l.url=p,console.log("exportData",l);const f={};return Object.keys(l).sort().forEach(e=>{f[e]=l[e]}),f}function calculateOriginalPrice(e,t){if(t>0){return e/(1-t/100)}return e}function convertCapRateToDecimal(e){if(!e||"Loading..."===e||"Not found"===e)return null;const t=e.match(/[\d.]+/);if(t){const n=parseFloat(t[0]);return e.includes("%")||n>1?Math.round(n/100*1e6)/1e6:Math.round(1e6*n)/1e6}return null}function formatDownPaymentPercent(e){return Math.round(e/100*1e6)/1e6}export{calculateOriginalPrice,convertCapRateToDecimal,createExportObjectCore,formatDownPaymentPercent};
|
|
2
|
+
//# sourceMappingURL=export-logic.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"export-logic.js","sources":["../../src/export/export-logic.js"],"sourcesContent":["// Map the on-screen property type to the dashboard's DB enum. Only \"multifamily\" -> \"mfr\"\r\n// actually differs; the rest pass through. Mirrors property-dashboard/validation/property.js\r\n// mapPropertyType (same table, same unknown -> \"mfr\" default) so the export URL carries the\r\n// real enum instead of relying on the server to convert it.\r\nfunction mapPropertyType(type) {\r\n const typeMap = {\r\n assisted: \"assisted\",\r\n business: \"business\",\r\n mixed_use: \"mixed_use\",\r\n multifamily: \"mfr\",\r\n rv_park: \"rv_park\",\r\n str: \"str\",\r\n };\r\n return typeMap[type] || \"mfr\";\r\n}\r\n\r\n// Pure business logic for data export - no DOM, no Chrome APIs.\r\n// Returns null to REFUSE export when the price was defaulted (no real price found):\r\n// a fabricated price would flow into NOI and silently land garbage in the dashboard.\r\nexport function createExportObjectCore(data, options = {}) {\r\n const {\r\n cachedEquity = null,\r\n currentDownPaymentPercent,\r\n currentInterestRateType = \"dscr_residential\",\r\n currentPriceDiscount = 0,\r\n currentPropertyType = \"str\",\r\n equitySource = \"scraped\",\r\n isUsingEstimatedCapRate = false,\r\n numberOfUnits = 4,\r\n priceWasDefaulted = false,\r\n windowLocation = \"\",\r\n } = options;\r\n\r\n if (priceWasDefaulted) return null;\r\n\r\n const exportData = {};\r\n\r\n // 1. Address\r\n if (data.name && data.name !== \"Property Details\" && data.name !== \"Not found\") {\r\n exportData.address = data.name;\r\n }\r\n\r\n // 2. Cap Rate - convert to decimal\r\n if (data.capRate && data.capRate !== \"Loading...\" && data.capRate !== \"Not found\") {\r\n const capMatch = data.capRate.match(/[\\d.]+/);\r\n if (capMatch) {\r\n const numericValue = parseFloat(capMatch[0]);\r\n\r\n // If the original string contains %, it's a percentage that needs conversion\r\n // If it's already a small decimal (< 1), it's likely already in decimal format\r\n if (data.capRate.includes(\"%\") || numericValue > 1) {\r\n // Percentage format - convert to decimal\r\n exportData.capRate = Math.round((numericValue / 100) * 1000000) / 1000000;\r\n } else {\r\n // Already in decimal format - use as-is\r\n exportData.capRate = Math.round(numericValue * 1000000) / 1000000;\r\n }\r\n }\r\n }\r\n\r\n // 3. Cap Rate Source\r\n exportData.capRateSource = isUsingEstimatedCapRate ? \"estimated\" : \"scraped\";\r\n\r\n // 4. Contact name\r\n if (data.contact && data.contact !== \"Not found\") {\r\n exportData.contact = data.contact;\r\n }\r\n\r\n // 5. Date Listed\r\n if (data.listingDate && data.listingDate !== \"Not found\") {\r\n exportData.dateListed = data.listingDate;\r\n }\r\n\r\n // 6. Price - calculate original price if discount applied\r\n if (data.price && data.price !== \"Loading...\" && data.price !== \"Not found\") {\r\n const priceMatch = data.price.match(/[\\d,]+/);\r\n if (priceMatch) {\r\n const displayedPrice = parseFloat(priceMatch[0].replace(/,/g, \"\"));\r\n\r\n if (currentPriceDiscount > 0) {\r\n const discountDecimal = currentPriceDiscount / 100;\r\n const originalPrice = displayedPrice / (1 - discountDecimal);\r\n exportData.price = Math.round(originalPrice);\r\n } else {\r\n exportData.price = displayedPrice;\r\n }\r\n }\r\n }\r\n\r\n // 7. Down Payment Percent (user-controlled value)\r\n if (currentDownPaymentPercent !== undefined) {\r\n exportData.downPaymentPercent = Math.round((currentDownPaymentPercent / 100) * 1000000) / 1000000;\r\n }\r\n\r\n // 8. Equity Percent\r\n if (cachedEquity && cachedEquity !== \"Loading...\") {\r\n const equityMatch = cachedEquity.match(/[\\d.]+/);\r\n if (equityMatch) {\r\n exportData.equityPercent = Math.round((parseFloat(equityMatch[0]) / 100) * 1000000) / 1000000;\r\n }\r\n }\r\n\r\n // 9. Equity Source\r\n exportData.equitySource = equitySource;\r\n\r\n // 10. Number of Units\r\n exportData.numberOfUnits = numberOfUnits;\r\n\r\n // 11. Phone number\r\n if (data.phone && data.phone !== \"Not found\") {\r\n exportData.phone = data.phone;\r\n }\r\n\r\n // 11. Price Discount Percent\r\n if (currentPriceDiscount > 0) {\r\n exportData.priceDiscountPercent = Math.round((currentPriceDiscount / 100) * 1000000) / 1000000;\r\n } else {\r\n exportData.priceDiscountPercent = 0;\r\n }\r\n\r\n // 12. Interest Rate Type\r\n exportData.interestRateType = currentInterestRateType;\r\n\r\n // 13. Property Type - mapped to the DB enum (multifamily -> mfr; rest pass through)\r\n exportData.propertyType = mapPropertyType(currentPropertyType);\r\n\r\n // 13. URL\r\n exportData.url = windowLocation;\r\n\r\n console.log(\"exportData\", exportData);\r\n\r\n // Alphabetize keys\r\n const alphabetized = {};\r\n Object.keys(exportData).sort().forEach(key => {\r\n alphabetized[key] = exportData[key];\r\n });\r\n\r\n return alphabetized;\r\n}\r\n\r\n// Pure calculation functions\r\nexport function calculateOriginalPrice(displayedPrice, discountPercent) {\r\n if (discountPercent > 0) {\r\n const discountDecimal = discountPercent / 100;\r\n return displayedPrice / (1 - discountDecimal);\r\n }\r\n return displayedPrice;\r\n}\r\n\r\nexport function convertCapRateToDecimal(capRateString) {\r\n if (!capRateString || capRateString === \"Loading...\" || capRateString === \"Not found\") {\r\n return null;\r\n }\r\n\r\n const capMatch = capRateString.match(/[\\d.]+/);\r\n if (capMatch) {\r\n const numericValue = parseFloat(capMatch[0]);\r\n\r\n if (capRateString.includes(\"%\") || numericValue > 1) {\r\n return Math.round((numericValue / 100) * 1000000) / 1000000;\r\n } else {\r\n return Math.round(numericValue * 1000000) / 1000000;\r\n }\r\n }\r\n\r\n return null;\r\n}\r\n\r\nexport function formatDownPaymentPercent(percentage) {\r\n return Math.round((percentage / 100) * 1000000) / 1000000;\r\n}\r\n"],"names":["createExportObjectCore","data","options","cachedEquity","currentDownPaymentPercent","currentInterestRateType","currentPriceDiscount","currentPropertyType","equitySource","isUsingEstimatedCapRate","numberOfUnits","priceWasDefaulted","windowLocation","exportData","name","address","capRate","capMatch","match","numericValue","parseFloat","includes","Math","round","capRateSource","contact","listingDate","dateListed","price","priceMatch","displayedPrice","replace","originalPrice","undefined","downPaymentPercent","equityMatch","equityPercent","phone","priceDiscountPercent","interestRateType","propertyType","assisted","business","mixed_use","multifamily","rv_park","str","url","console","log","alphabetized","Object","keys","sort","forEach","key","calculateOriginalPrice","discountPercent","convertCapRateToDecimal","capRateString","formatDownPaymentPercent","percentage"],"mappings":"AAmBO,SAASA,uBAAuBC,EAAMC,EAAU,IACrD,MAAMC,aACJA,EAAe,KAAIC,0BACnBA,EAAyBC,wBACzBA,EAA0B,mBAAkBC,qBAC5CA,EAAuB,EAACC,oBACxBA,EAAsB,MAAKC,aAC3BA,EAAe,UAASC,wBACxBA,GAA0B,EAAKC,cAC/BA,EAAgB,EAACC,kBACjBA,GAAoB,EAAKC,eACzBA,EAAiB,IACfV,EAEJ,GAAIS,EAAmB,OAAO,KAE9B,MAAME,EAAa,CAAA,EAQnB,GALIZ,EAAKa,MAAsB,qBAAdb,EAAKa,MAA6C,cAAdb,EAAKa,OACxDD,EAAWE,QAAUd,EAAKa,MAIxBb,EAAKe,SAA4B,eAAjBf,EAAKe,SAA6C,cAAjBf,EAAKe,QAAyB,CACjF,MAAMC,EAAWhB,EAAKe,QAAQE,MAAM,UACpC,GAAID,EAAU,CACZ,MAAME,EAAeC,WAAWH,EAAS,IAIrChB,EAAKe,QAAQK,SAAS,MAAQF,EAAe,EAE/CN,EAAWG,QAAUM,KAAKC,MAAOJ,EAAe,IAAO,KAAW,IAGlEN,EAAWG,QAAUM,KAAKC,MAAqB,IAAfJ,GAA0B,GAE9D,CACF,CAgBA,GAbAN,EAAWW,cAAgBf,EAA0B,YAAc,UAG/DR,EAAKwB,SAA4B,cAAjBxB,EAAKwB,UACvBZ,EAAWY,QAAUxB,EAAKwB,SAIxBxB,EAAKyB,aAAoC,cAArBzB,EAAKyB,cAC3Bb,EAAWc,WAAa1B,EAAKyB,aAI3BzB,EAAK2B,OAAwB,eAAf3B,EAAK2B,OAAyC,cAAf3B,EAAK2B,MAAuB,CAC3E,MAAMC,EAAa5B,EAAK2B,MAAMV,MAAM,UACpC,GAAIW,EAAY,CACd,MAAMC,EAAiBV,WAAWS,EAAW,GAAGE,QAAQ,KAAM,KAE9D,GAAIzB,EAAuB,EAAG,CAC5B,MACM0B,EAAgBF,GAAkB,EADhBxB,EAAuB,KAE/CO,EAAWe,MAAQN,KAAKC,MAAMS,EAChC,MACEnB,EAAWe,MAAQE,CAEvB,CACF,CAQA,QALkCG,IAA9B7B,IACFS,EAAWqB,mBAAqBZ,KAAKC,MAAOnB,EAA4B,IAAO,KAAW,KAIxFD,GAAiC,eAAjBA,EAA+B,CACjD,MAAMgC,EAAchC,EAAae,MAAM,UACnCiB,IACFtB,EAAWuB,cAAgBd,KAAKC,MAAOH,WAAWe,EAAY,IAAM,IAAO,KAAW,IAE1F,CAGAtB,EAAWL,aAAeA,EAG1BK,EAAWH,cAAgBA,EAGvBT,EAAKoC,OAAwB,cAAfpC,EAAKoC,QACrBxB,EAAWwB,MAAQpC,EAAKoC,OAKxBxB,EAAWyB,qBADThC,EAAuB,EACSgB,KAAKC,MAAOjB,EAAuB,IAAO,KAAW,IAErD,EAIpCO,EAAW0B,iBAAmBlC,EAG9BQ,EAAW2B,aAvHK,CACdC,SAAU,WACVC,SAAU,WACVC,UAAW,YACXC,YAAa,MACbC,QAAS,UACTC,IAAK,OAiHmCvC,IA/GlB,MAkHxBM,EAAWkC,IAAMnC,EAEjBoC,QAAQC,IAAI,aAAcpC,GAG1B,MAAMqC,EAAe,CAAA,EAKrB,OAJAC,OAAOC,KAAKvC,GAAYwC,OAAOC,QAAQC,IACrCL,EAAaK,GAAO1C,EAAW0C,KAG1BL,CACT,CAGO,SAASM,uBAAuB1B,EAAgB2B,GACrD,GAAIA,EAAkB,EAAG,CAEvB,OAAO3B,GAAkB,EADD2B,EAAkB,IAE5C,CACA,OAAO3B,CACT,CAEO,SAAS4B,wBAAwBC,GACtC,IAAKA,GAAmC,eAAlBA,GAAoD,cAAlBA,EACtD,OAAO,KAGT,MAAM1C,EAAW0C,EAAczC,MAAM,UACrC,GAAID,EAAU,CACZ,MAAME,EAAeC,WAAWH,EAAS,IAEzC,OAAI0C,EAActC,SAAS,MAAQF,EAAe,EACzCG,KAAKC,MAAOJ,EAAe,IAAO,KAAW,IAE7CG,KAAKC,MAAqB,IAAfJ,GAA0B,GAEhD,CAEA,OAAO,IACT,CAEO,SAASyC,yBAAyBC,GACvC,OAAOvC,KAAKC,MAAOsC,EAAa,IAAO,KAAW,GACpD"}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{FINANCIAL_CONSTANTS as t}from"../config/financial.js";import{BUSINESS_CONSTANTS as e}from"../config/business.js";import{PROPERTY_TYPES as c,PROPERTY_TYPE_CONSTANTS as a}from"../config/property-types.js";const r=t.INTEREST_RATE_TIERS[t.DEFAULT_INTEREST_RATE_TYPE];function calculatePMT(t,e,c){if(0===e)return t/(12*c);const a=e/12,r=12*c;return t*(a*Math.pow(1+a,r))/(Math.pow(1+a,r)-1)}function calculateCOCR30(t,e){try{const c=.3*t,a=12*calculatePMT(.7*t,.075,30);return(e-a)/c*100}catch(t){return 0}}function calculateCashFlowYield(t,e){if(!e||e<=0)return 0;return 12*t/e*100}function calculatePriceForCOCR(c,a=.15,n={}){const{downPercent:l=100*t.DEFAULT_DOWN_PAYMENT,dscrLtvPercent:
|
|
1
|
+
import{FINANCIAL_CONSTANTS as t}from"../config/financial.js";import{BUSINESS_CONSTANTS as e}from"../config/business.js";import{PROPERTY_TYPES as c,PROPERTY_TYPE_CONSTANTS as a}from"../config/property-types.js";const r=t.INTEREST_RATE_TIERS[t.DEFAULT_INTEREST_RATE_TYPE];function calculatePMT(t,e,c){if(0===e)return t/(12*c);const a=e/12,r=12*c;return t*(a*Math.pow(1+a,r))/(Math.pow(1+a,r)-1)}function calculateCOCR30(t,e){try{const c=.3*t,a=12*calculatePMT(.7*t,.075,30);return(e-a)/c*100}catch(t){return 0}}function calculateCashFlowYield(t,e){if(!e||e<=0)return 0;return 12*t/e*100}function calculatePriceForCOCR(c,a=.15,n={}){const{downPercent:l=100*t.DEFAULT_DOWN_PAYMENT,dscrLtvPercent:u=100*t.DEFAULT_DSCR_PERCENTAGE,dscrRate:o=r.rate,dscrTerm:E=r.amortization,maxIterations:i=e.MAX_ITERATIONS,tolerance:s=e.CALCULATION_TOLERANCE}=n;try{let t=c/.08,r=0;for(;r<i;){const n=t*(l/100),i=12*calculatePMT(t*(u/100),o,E),T=(c-i)/n;if(Math.abs(T-a)<s)break;const R=T-a,A=R*e.ADJUSTMENT_FACTOR;t*=R>0?1+Math.abs(A):1-Math.abs(A),t<1e3&&(t=1e3),t>c*e.MAX_COCR15_PRICE_MULTIPLIER&&(t=c*e.CONSERVATIVE_COCR15_PRICE_MULTIPLIER),r++}return t<e.MINIMUM_COCR15_PRICE&&(t=e.MINIMUM_COCR15_PRICE),t}catch(t){return 0}}function calculateCOCRAtPercent(t,e,c,a={}){const{dscrRate:n=r.rate,dscrTerm:l=r.amortization}=a;try{const a=t*(c/100),r=12*calculatePMT(t-a,n,l);return(e-r)/a*100}catch(t){return 0}}function calculateSTRNOI(t,e=null,c={}){const{grossRate:r=a.STR.ESTIMATED_GROSS_RATE,noiPercentage:n=a.STR.NOI_PERCENTAGE}=c;try{if(e&&Number.isFinite(e.value)&&e.value>=0){if("noi"===e.type)return e.value;if("gross"===e.type)return e.value*n}return!Number.isFinite(t)||t<=0?0:t*r*n}catch(t){return 0}}function calculateNOIByType(t,e,r=c.MULTIFAMILY,n={}){const{strApiResult:l=null,strGrossIncomeMultiplier:u=a.STR.ESTIMATED_GROSS_RATE,strNoiPercentage:o=a.STR.NOI_PERCENTAGE,assistedIncomePerBedroom:E=a.ASSISTED_LIVING.INCOME_PER_BEDROOM_MONTHLY,bedroomCount:i=a.ASSISTED_LIVING.DEFAULT_BEDROOM_COUNT}=n;try{switch(r.toLowerCase()){case c.STR:return calculateSTRNOI(t,l,{grossRate:u,noiPercentage:o});case c.ASSISTED_LIVING:return i*E*12;case c.MULTIFAMILY:default:return t*e}}catch(t){return 0}}function calculateAssignmentFee(t,c=100*e.ASSIGNMENT_FEE_PERCENTAGE){try{return t*(c/100)}catch(t){return 0}}function calculateNetToBuyer(c,a={}){const{buyerCostPercent:r=100*e.NET_TO_BUYER_PERCENTAGE,sellerCostAssignment:n=100*e.ASSIGNMENT_FEE_PERCENTAGE,sellerCostClosing:l=100*e.CLOSING_COSTS_PERCENTAGE,additionalCostRehab:u=100*e.REHAB_RATE,additionalCostFinancing:o=100*e.HARD_MONEY_RATE,dscrLtvPercent:E=100*t.DEFAULT_DSCR_PERCENTAGE}=a;try{return c*(r/100)-c*((n+l)/100)-c*(u/100)-o/100*(c-c*(E/100))}catch(t){return 0}}function calculateBalloonBalance(e,c,a,r=t.DEFAULT_BALLOON_PERIOD_YEARS){try{if(e<=0||c<0||a<=0||r<=0)return 0;if(r>=a)return 0;if(0===c){const t=12*a;return e*(t-12*r)/t}const t=c/12,n=12*a,l=12*r,u=Math.pow(1+t,n),o=e*(u-Math.pow(1+t,l))/(u-1);return Math.max(0,o)}catch(t){return 0}}function calculateAppreciatedValue(e,c=t.APPRECIATION_RATE,a=t.DEFAULT_BALLOON_PERIOD_YEARS){try{return e<=0||c<0||a<0?e:e*Math.pow(1+c,a)}catch(t){return e}}function calculateCashOutAfterRefi(e,c,a,n={}){const{appreciationRate:l=t.APPRECIATION_RATE,balloonYears:u=t.DEFAULT_BALLOON_PERIOD_YEARS,dscrRate:o=r.rate,dscrTerm:E=r.amortization,sellerFiTerm:i=t.SELLER_FI_AMORTIZATION,refiLtvPercent:s=70}=n;try{const r=calculateAppreciatedValue(e,l,u),n=calculateBalloonBalance(c,o,E,u),T=calculateBalloonBalance(a,t.SELLER_FI_INTEREST_RATE,i,u);return r*(s/100)-(n+T)}catch(t){return 0}}function calculateCashFlow(t,e,c){return t-(e+c)}function calculateDiscountFromPrice(t,e){return!t||t<=0?0:(t-e)/t}function calculatePriceFromDiscount(t,e){return!t||t<=0?0:t*(1-e)}function safePercentage(t,e=100){return null==t||isNaN(t)?e:100*t}export{calculateAppreciatedValue,calculateAssignmentFee,calculateBalloonBalance,calculateCOCR30,calculateCOCRAtPercent,calculateCashFlow,calculateCashFlowYield,calculateCashOutAfterRefi,calculateDiscountFromPrice,calculateNOIByType,calculateNetToBuyer,calculatePMT,calculatePriceForCOCR,calculatePriceFromDiscount,calculateSTRNOI,safePercentage};
|
|
2
2
|
//# sourceMappingURL=calculations.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"calculations.js","sources":["../../src/financial/calculations.js"],"sourcesContent":["// src/financial/calculations.js\r\n\r\nimport { FINANCIAL_CONSTANTS } from '../config/financial.js';\r\nimport { BUSINESS_CONSTANTS } from '../config/business.js';\r\nimport { PROPERTY_TYPE_CONSTANTS, PROPERTY_TYPES } from '../config/property-types.js';\r\n\r\nconst DEFAULT_TIER = FINANCIAL_CONSTANTS.INTEREST_RATE_TIERS[FINANCIAL_CONSTANTS.DEFAULT_INTEREST_RATE_TYPE];\r\n\r\n\r\n/**\r\n * PMT function for loan payment calculation\r\n * @param {number} principal - Loan principal amount \r\n * @param {number} annualRate - Annual interest rate (as decimal, e.g., 0.075 for 7.5%)\r\n * @param {number} years - Loan term in years\r\n * @returns {number} Monthly payment amount\r\n */\r\nexport function calculatePMT(principal, annualRate, years) {\r\n if (annualRate === 0) {\r\n return principal / (years * 12);\r\n }\r\n \r\n const monthlyRate = annualRate / 12;\r\n const numPayments = years * 12;\r\n const pmt = principal * (monthlyRate * Math.pow(1 + monthlyRate, numPayments)) / \r\n (Math.pow(1 + monthlyRate, numPayments) - 1);\r\n return pmt;\r\n}\r\n\r\nexport function calculateCOCR30(askingPrice, noi) {\r\n try {\r\n const cashInvested = askingPrice * 0.30; // 30% down payment\r\n const dscrLoanAmount = askingPrice * 0.70; // Fixed 70% DSCR loan\r\n const dscrPayment = calculatePMT(dscrLoanAmount, 0.075, 30) * 12; // Annual DSCR payment\r\n const annualCashFlow = noi - dscrPayment;\r\n const cocr = (annualCashFlow / cashInvested) * 100;\r\n \r\n return cocr;\r\n } catch (error) {\r\n return 0;\r\n }\r\n}\r\n\r\nexport function calculateCashFlowYield(monthlyCashFlow, purchasePrice) {\r\n if (!purchasePrice || purchasePrice <= 0) return 0;\r\n const annualCashFlow = monthlyCashFlow * 12;\r\n return (annualCashFlow / purchasePrice) * 100;\r\n}\r\n\r\n\r\n/**\r\n * Calculate the property price that yields a target COCR percentage\r\n * @param {number} noi - Net Operating Income (annual)\r\n * @param {number} targetCOCR - Target COCR as decimal (default: 0.15 for 15%)\r\n * @param {Object} options - Configuration options (uses config constants as defaults)\r\n * @returns {number} Calculated property price\r\n */\r\nexport function calculatePriceForCOCR(noi, targetCOCR = 0.15, options = {}) {\r\n const {\r\n downPercent = FINANCIAL_CONSTANTS.DEFAULT_DOWN_PAYMENT * 100,\r\n dscrLtvPercent = FINANCIAL_CONSTANTS.DEFAULT_DSCR_PERCENTAGE * 100,\r\n dscrRate = DEFAULT_TIER.rate,\r\n dscrTerm = DEFAULT_TIER.amortization,\r\n maxIterations = BUSINESS_CONSTANTS.MAX_ITERATIONS,\r\n tolerance = BUSINESS_CONSTANTS.CALCULATION_TOLERANCE\r\n } = options;\r\n\r\n try {\r\n let targetPrice = noi / 0.08; // Initial estimate: NOI / 8% cap rate\r\n let iterations = 0;\r\n \r\n while (iterations < maxIterations) {\r\n const cashInvested = targetPrice * (downPercent / 100);\r\n const dscrLoanAmount = targetPrice * (dscrLtvPercent / 100);\r\n const dscrPayment = calculatePMT(dscrLoanAmount, dscrRate, dscrTerm) * 12;\r\n const annualCashFlow = noi - dscrPayment;\r\n const currentCOCR = annualCashFlow / cashInvested;\r\n \r\n if (Math.abs(currentCOCR - targetCOCR) < tolerance) {\r\n break;\r\n }\r\n \r\n const error = currentCOCR - targetCOCR;\r\n const adjustment = error * BUSINESS_CONSTANTS.ADJUSTMENT_FACTOR;\r\n \r\n if (error > 0) {\r\n targetPrice = targetPrice * (1 + Math.abs(adjustment));\r\n } else {\r\n targetPrice = targetPrice * (1 - Math.abs(adjustment));\r\n }\r\n \r\n // Reasonable bounds during iteration (prevent extreme values)\r\n if (targetPrice < 1000) targetPrice = 1000;\r\n if (targetPrice > noi * BUSINESS_CONSTANTS.MAX_COCR15_PRICE_MULTIPLIER) {\r\n targetPrice = noi * BUSINESS_CONSTANTS.CONSERVATIVE_COCR15_PRICE_MULTIPLIER;\r\n }\r\n \r\n iterations++;\r\n }\r\n \r\n // Apply final bounds check AFTER iteration\r\n if (targetPrice < BUSINESS_CONSTANTS.MINIMUM_COCR15_PRICE) {\r\n targetPrice = BUSINESS_CONSTANTS.MINIMUM_COCR15_PRICE;\r\n }\r\n \r\n return targetPrice;\r\n } catch (error) {\r\n return 0;\r\n }\r\n}\r\n\r\n/**\r\n * Calculate COCR at a specific down payment percentage\r\n * @param {number} askingPrice - Property asking price\r\n * @param {number} noi - Net Operating Income (annual)\r\n * @param {number} downPercent - Down payment percentage\r\n * @param {Object} options - Configuration options (uses config constants as defaults)\r\n * @returns {number} COCR percentage\r\n */\r\nexport function calculateCOCRAtPercent(askingPrice, noi, downPercent, options = {}) {\r\n const {\r\n dscrRate = DEFAULT_TIER.rate,\r\n dscrTerm = DEFAULT_TIER.amortization,\r\n } = options;\r\n\r\n try {\r\n const downDecimal = downPercent / 100;\r\n const cashInvested = askingPrice * downDecimal;\r\n \r\n // Fix financing structure: seller financing reduces available DSCR loan\r\n const dscrLoanAmount = askingPrice - cashInvested;\r\n \r\n const dscrPayment = calculatePMT(dscrLoanAmount, dscrRate, dscrTerm) * 12;\r\n \r\n const annualCashFlow = noi - dscrPayment;\r\n const cocr = (annualCashFlow / cashInvested) * 100;\r\n \r\n return cocr;\r\n } catch (error) {\r\n return 0;\r\n }\r\n}\r\n\r\n/**\r\n * Calculate NOI based on property type\r\n * @param {number} askingPrice - Property asking price\r\n * @param {number} capRate - Cap rate as decimal (e.g., 0.08 for 8%)\r\n * @param {string} propertyType - Property type from PROPERTY_TYPES\r\n * @param {Object} options - Configuration options (uses config constants as defaults)\r\n * @returns {number} Calculated NOI\r\n */\r\nexport function calculateNOIByType(askingPrice, capRate, propertyType = PROPERTY_TYPES.MULTIFAMILY, options = {}) {\r\n const {\r\n strGrossIncomeMultiplier = PROPERTY_TYPE_CONSTANTS.STR.ESTIMATED_GROSS_RATE,\r\n strNoiPercentage = PROPERTY_TYPE_CONSTANTS.STR.NOI_PERCENTAGE,\r\n assistedIncomePerBedroom = PROPERTY_TYPE_CONSTANTS.ASSISTED_LIVING.INCOME_PER_BEDROOM_MONTHLY,\r\n bedroomCount = PROPERTY_TYPE_CONSTANTS.ASSISTED_LIVING.DEFAULT_BEDROOM_COUNT\r\n } = options;\r\n\r\n try {\r\n switch (propertyType.toLowerCase()) {\r\n case PROPERTY_TYPES.STR:\r\n const estimatedGrossIncome = askingPrice * strGrossIncomeMultiplier;\r\n return estimatedGrossIncome * strNoiPercentage;\r\n \r\n case PROPERTY_TYPES.ASSISTED_LIVING:\r\n return bedroomCount * assistedIncomePerBedroom * 12;\r\n \r\n case PROPERTY_TYPES.MULTIFAMILY:\r\n default:\r\n return askingPrice * capRate;\r\n }\r\n } catch (error) {\r\n return 0;\r\n }\r\n}\r\n\r\n/**\r\n * Calculate assignment fee\r\n * @param {number} askingPrice - Property asking price\r\n * @param {number} assignmentPercent - Assignment fee percentage (uses config default)\r\n * @returns {number} Assignment fee amount\r\n */\r\nexport function calculateAssignmentFee(askingPrice, assignmentPercent = BUSINESS_CONSTANTS.ASSIGNMENT_FEE_PERCENTAGE * 100) {\r\n try {\r\n return askingPrice * (assignmentPercent / 100);\r\n } catch (error) {\r\n return 0;\r\n }\r\n}\r\n\r\n/**\r\n * Calculate net to buyer\r\n * @param {number} askingPrice - Property asking price\r\n * @param {Object} options - Configuration options (uses config constants as defaults)\r\n * @returns {number} Net to buyer amount\r\n */\r\nexport function calculateNetToBuyer(askingPrice, options = {}) {\r\n const {\r\n buyerCostPercent = BUSINESS_CONSTANTS.NET_TO_BUYER_PERCENTAGE * 100,\r\n sellerCostAssignment = BUSINESS_CONSTANTS.ASSIGNMENT_FEE_PERCENTAGE * 100,\r\n sellerCostClosing = BUSINESS_CONSTANTS.CLOSING_COSTS_PERCENTAGE * 100,\r\n additionalCostRehab = BUSINESS_CONSTANTS.REHAB_RATE * 100,\r\n additionalCostFinancing = BUSINESS_CONSTANTS.HARD_MONEY_RATE * 100,\r\n dscrLtvPercent = FINANCIAL_CONSTANTS.DEFAULT_DSCR_PERCENTAGE * 100\r\n } = options;\r\n\r\n try {\r\n const dscrLoanAmount = askingPrice * (dscrLtvPercent / 100);\r\n \r\n return askingPrice * (buyerCostPercent / 100) - \r\n askingPrice * ((sellerCostAssignment + sellerCostClosing) / 100) - \r\n askingPrice * (additionalCostRehab / 100) - \r\n (additionalCostFinancing / 100) * (askingPrice - dscrLoanAmount);\r\n } catch (error) {\r\n return 0;\r\n }\r\n}\r\n\r\n/**\r\n * Calculate remaining loan balance at end of balloon period\r\n * @param {number} loanAmount - Initial loan amount\r\n * @param {number} interestRate - Annual interest rate as decimal (e.g., 0.075 for 7.5%)\r\n * @param {number} amortizationYears - Full amortization period in years\r\n * @param {number} balloonYears - Balloon period in years\r\n * @returns {number} Remaining balance at end of balloon period\r\n */\r\nexport function calculateBalloonBalance(loanAmount, interestRate, amortizationYears, balloonYears = FINANCIAL_CONSTANTS.DEFAULT_BALLOON_PERIOD_YEARS) {\r\n try {\r\n if (loanAmount <= 0 || interestRate < 0 || amortizationYears <= 0 || balloonYears <= 0) {\r\n return 0;\r\n }\r\n\r\n // If balloon period equals or exceeds amortization, loan is fully paid\r\n if (balloonYears >= amortizationYears) {\r\n return 0;\r\n }\r\n\r\n // Special handling for zero interest rate (simple linear paydown)\r\n if (interestRate === 0) {\r\n const totalPayments = amortizationYears * 12;\r\n const paymentsMade = balloonYears * 12;\r\n return loanAmount * (totalPayments - paymentsMade) / totalPayments;\r\n }\r\n\r\n const monthlyRate = interestRate / 12;\r\n const totalPayments = amortizationYears * 12;\r\n const balloonPayments = balloonYears * 12;\r\n\r\n // Calculate remaining balance using loan balance formula\r\n // Balance = P * [(1 + r)^n - (1 + r)^p] / [(1 + r)^n - 1]\r\n // Where P = principal, r = monthly rate, n = total payments, p = payments made\r\n \r\n const factor1 = Math.pow(1 + monthlyRate, totalPayments);\r\n const factor2 = Math.pow(1 + monthlyRate, balloonPayments);\r\n \r\n const remainingBalance = loanAmount * (factor1 - factor2) / (factor1 - 1);\r\n \r\n return Math.max(0, remainingBalance); // Ensure non-negative\r\n } catch (error) {\r\n return 0;\r\n }\r\n}\r\n\r\n/**\r\n * Calculate property value after appreciation period\r\n * @param {number} currentValue - Current property value\r\n * @param {number} appreciationRate - Annual appreciation rate as decimal\r\n * @param {number} years - Number of years\r\n * @returns {number} Appreciated property value\r\n */\r\nexport function calculateAppreciatedValue(currentValue, appreciationRate = FINANCIAL_CONSTANTS.APPRECIATION_RATE, years = FINANCIAL_CONSTANTS.DEFAULT_BALLOON_PERIOD_YEARS) {\r\n try {\r\n if (currentValue <= 0 || appreciationRate < 0 || years < 0) {\r\n return currentValue;\r\n }\r\n \r\n return currentValue * Math.pow(1 + appreciationRate, years);\r\n } catch (error) {\r\n return currentValue;\r\n }\r\n}\r\n\r\n/**\r\n * Calculate cash out amount after appreciation refinance\r\n * @param {number} originalPrice - Original purchase price\r\n * @param {number} dscrLoanAmount - Original DSCR loan amount \r\n * @param {number} sellerFiAmount - Original seller financing amount\r\n * @param {Object} options - Configuration options\r\n * @returns {number} Cash out amount (positive = cash out, negative = cash in)\r\n */\r\nexport function calculateCashOutAfterRefi(originalPrice, dscrLoanAmount, sellerFiAmount, options = {}) {\r\n const {\r\n appreciationRate = FINANCIAL_CONSTANTS.APPRECIATION_RATE,\r\n balloonYears = FINANCIAL_CONSTANTS.DEFAULT_BALLOON_PERIOD_YEARS,\r\n dscrRate = DEFAULT_TIER.rate,\r\n dscrTerm = DEFAULT_TIER.amortization,\r\n sellerFiTerm = FINANCIAL_CONSTANTS.SELLER_FI_AMORTIZATION,\r\n refiLtvPercent = 70 // 70% LTV on refi\r\n } = options;\r\n\r\n try {\r\n // Calculate appreciated property value\r\n const appreciatedValue = calculateAppreciatedValue(originalPrice, appreciationRate, balloonYears);\r\n \r\n // Calculate remaining balance on DSCR loan\r\n const dscrRemainingBalance = calculateBalloonBalance(dscrLoanAmount, dscrRate, dscrTerm, balloonYears);\r\n \r\n // Calculate remaining balance on seller financing (0% interest)\r\n const sellerFiRemainingBalance = calculateBalloonBalance(sellerFiAmount, FINANCIAL_CONSTANTS.SELLER_FI_INTEREST_RATE, sellerFiTerm, balloonYears);\r\n \r\n // Total remaining debt\r\n const totalRemainingDebt = dscrRemainingBalance + sellerFiRemainingBalance;\r\n \r\n // Calculate new loan amount at 70% LTV of appreciated value\r\n const newLoanAmount = appreciatedValue * (refiLtvPercent / 100);\r\n \r\n // Cash out = new loan - total remaining debt\r\n const cashOut = newLoanAmount - totalRemainingDebt;\r\n \r\n return cashOut;\r\n } catch (error) {\r\n return 0;\r\n }\r\n}\r\n\r\n// Cash Flow calculation (matching loopnet-analyzer exactly)\r\nexport function calculateCashFlow(monthlyNOI, dscrPayment, sfPayment) {\r\n return monthlyNOI - (dscrPayment + sfPayment);\r\n}\r\n\r\n/**\r\n * Calculate discount percentage from asking price and offered price\r\n * @param {number} askingPrice - Property asking price\r\n * @param {number} priceOffered - Offered price\r\n * @returns {number} Discount as decimal (positive = discount, negative = premium)\r\n */\r\nexport function calculateDiscountFromPrice(askingPrice, priceOffered) {\r\n if (!askingPrice || askingPrice <= 0) return 0;\r\n \r\n return (askingPrice - priceOffered) / askingPrice;\r\n}\r\n\r\n/**\r\n * Calculate price from asking price and discount percentage\r\n * @param {number} askingPrice - Property asking price \r\n * @param {number} discountPercent - Discount as decimal (positive = discount, negative = premium)\r\n * @returns {number} Calculated price\r\n */\r\nexport function calculatePriceFromDiscount(askingPrice, discountPercent) {\r\n if (!askingPrice || askingPrice <= 0) return 0;\r\n \r\n return askingPrice * (1 - discountPercent);\r\n}\r\n\r\nexport function safePercentage(value, fallback = 100) {\r\n return (value != null && !isNaN(value)) ? (value * 100) : fallback;\r\n}"],"names":["DEFAULT_TIER","FINANCIAL_CONSTANTS","INTEREST_RATE_TIERS","DEFAULT_INTEREST_RATE_TYPE","calculatePMT","principal","annualRate","years","monthlyRate","numPayments","Math","pow","calculateCOCR30","askingPrice","noi","cashInvested","dscrPayment","error","calculateCashFlowYield","monthlyCashFlow","purchasePrice","calculatePriceForCOCR","targetCOCR","options","downPercent","DEFAULT_DOWN_PAYMENT","dscrLtvPercent","DEFAULT_DSCR_PERCENTAGE","dscrRate","rate","dscrTerm","amortization","maxIterations","BUSINESS_CONSTANTS","MAX_ITERATIONS","tolerance","CALCULATION_TOLERANCE","targetPrice","iterations","currentCOCR","abs","adjustment","ADJUSTMENT_FACTOR","MAX_COCR15_PRICE_MULTIPLIER","CONSERVATIVE_COCR15_PRICE_MULTIPLIER","MINIMUM_COCR15_PRICE","calculateCOCRAtPercent","calculateNOIByType","capRate","propertyType","PROPERTY_TYPES","MULTIFAMILY","strGrossIncomeMultiplier","PROPERTY_TYPE_CONSTANTS","STR","ESTIMATED_GROSS_RATE","strNoiPercentage","NOI_PERCENTAGE","assistedIncomePerBedroom","ASSISTED_LIVING","INCOME_PER_BEDROOM_MONTHLY","bedroomCount","DEFAULT_BEDROOM_COUNT","toLowerCase","calculateAssignmentFee","assignmentPercent","ASSIGNMENT_FEE_PERCENTAGE","calculateNetToBuyer","buyerCostPercent","NET_TO_BUYER_PERCENTAGE","sellerCostAssignment","sellerCostClosing","CLOSING_COSTS_PERCENTAGE","additionalCostRehab","REHAB_RATE","additionalCostFinancing","HARD_MONEY_RATE","calculateBalloonBalance","loanAmount","interestRate","amortizationYears","balloonYears","DEFAULT_BALLOON_PERIOD_YEARS","totalPayments","balloonPayments","factor1","remainingBalance","max","calculateAppreciatedValue","currentValue","appreciationRate","APPRECIATION_RATE","calculateCashOutAfterRefi","originalPrice","dscrLoanAmount","sellerFiAmount","sellerFiTerm","SELLER_FI_AMORTIZATION","refiLtvPercent","appreciatedValue","dscrRemainingBalance","sellerFiRemainingBalance","SELLER_FI_INTEREST_RATE","calculateCashFlow","monthlyNOI","sfPayment","calculateDiscountFromPrice","priceOffered","calculatePriceFromDiscount","discountPercent","safePercentage","value","fallback","isNaN"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"calculations.js","sources":["../../src/financial/calculations.js"],"sourcesContent":["// src/financial/calculations.js\r\n\r\nimport { FINANCIAL_CONSTANTS } from '../config/financial.js';\r\nimport { BUSINESS_CONSTANTS } from '../config/business.js';\r\nimport { PROPERTY_TYPE_CONSTANTS, PROPERTY_TYPES } from '../config/property-types.js';\r\n\r\nconst DEFAULT_TIER = FINANCIAL_CONSTANTS.INTEREST_RATE_TIERS[FINANCIAL_CONSTANTS.DEFAULT_INTEREST_RATE_TYPE];\r\n\r\n\r\n/**\r\n * PMT function for loan payment calculation\r\n * @param {number} principal - Loan principal amount \r\n * @param {number} annualRate - Annual interest rate (as decimal, e.g., 0.075 for 7.5%)\r\n * @param {number} years - Loan term in years\r\n * @returns {number} Monthly payment amount\r\n */\r\nexport function calculatePMT(principal, annualRate, years) {\r\n if (annualRate === 0) {\r\n return principal / (years * 12);\r\n }\r\n \r\n const monthlyRate = annualRate / 12;\r\n const numPayments = years * 12;\r\n const pmt = principal * (monthlyRate * Math.pow(1 + monthlyRate, numPayments)) / \r\n (Math.pow(1 + monthlyRate, numPayments) - 1);\r\n return pmt;\r\n}\r\n\r\nexport function calculateCOCR30(askingPrice, noi) {\r\n try {\r\n const cashInvested = askingPrice * 0.30; // 30% down payment\r\n const dscrLoanAmount = askingPrice * 0.70; // Fixed 70% DSCR loan\r\n const dscrPayment = calculatePMT(dscrLoanAmount, 0.075, 30) * 12; // Annual DSCR payment\r\n const annualCashFlow = noi - dscrPayment;\r\n const cocr = (annualCashFlow / cashInvested) * 100;\r\n \r\n return cocr;\r\n } catch (error) {\r\n return 0;\r\n }\r\n}\r\n\r\nexport function calculateCashFlowYield(monthlyCashFlow, purchasePrice) {\r\n if (!purchasePrice || purchasePrice <= 0) return 0;\r\n const annualCashFlow = monthlyCashFlow * 12;\r\n return (annualCashFlow / purchasePrice) * 100;\r\n}\r\n\r\n\r\n/**\r\n * Calculate the property price that yields a target COCR percentage\r\n * @param {number} noi - Net Operating Income (annual)\r\n * @param {number} targetCOCR - Target COCR as decimal (default: 0.15 for 15%)\r\n * @param {Object} options - Configuration options (uses config constants as defaults)\r\n * @returns {number} Calculated property price\r\n */\r\nexport function calculatePriceForCOCR(noi, targetCOCR = 0.15, options = {}) {\r\n const {\r\n downPercent = FINANCIAL_CONSTANTS.DEFAULT_DOWN_PAYMENT * 100,\r\n dscrLtvPercent = FINANCIAL_CONSTANTS.DEFAULT_DSCR_PERCENTAGE * 100,\r\n dscrRate = DEFAULT_TIER.rate,\r\n dscrTerm = DEFAULT_TIER.amortization,\r\n maxIterations = BUSINESS_CONSTANTS.MAX_ITERATIONS,\r\n tolerance = BUSINESS_CONSTANTS.CALCULATION_TOLERANCE\r\n } = options;\r\n\r\n try {\r\n let targetPrice = noi / 0.08; // Initial estimate: NOI / 8% cap rate\r\n let iterations = 0;\r\n \r\n while (iterations < maxIterations) {\r\n const cashInvested = targetPrice * (downPercent / 100);\r\n const dscrLoanAmount = targetPrice * (dscrLtvPercent / 100);\r\n const dscrPayment = calculatePMT(dscrLoanAmount, dscrRate, dscrTerm) * 12;\r\n const annualCashFlow = noi - dscrPayment;\r\n const currentCOCR = annualCashFlow / cashInvested;\r\n \r\n if (Math.abs(currentCOCR - targetCOCR) < tolerance) {\r\n break;\r\n }\r\n \r\n const error = currentCOCR - targetCOCR;\r\n const adjustment = error * BUSINESS_CONSTANTS.ADJUSTMENT_FACTOR;\r\n \r\n if (error > 0) {\r\n targetPrice = targetPrice * (1 + Math.abs(adjustment));\r\n } else {\r\n targetPrice = targetPrice * (1 - Math.abs(adjustment));\r\n }\r\n \r\n // Reasonable bounds during iteration (prevent extreme values)\r\n if (targetPrice < 1000) targetPrice = 1000;\r\n if (targetPrice > noi * BUSINESS_CONSTANTS.MAX_COCR15_PRICE_MULTIPLIER) {\r\n targetPrice = noi * BUSINESS_CONSTANTS.CONSERVATIVE_COCR15_PRICE_MULTIPLIER;\r\n }\r\n \r\n iterations++;\r\n }\r\n \r\n // Apply final bounds check AFTER iteration\r\n if (targetPrice < BUSINESS_CONSTANTS.MINIMUM_COCR15_PRICE) {\r\n targetPrice = BUSINESS_CONSTANTS.MINIMUM_COCR15_PRICE;\r\n }\r\n \r\n return targetPrice;\r\n } catch (error) {\r\n return 0;\r\n }\r\n}\r\n\r\n/**\r\n * Calculate COCR at a specific down payment percentage\r\n * @param {number} askingPrice - Property asking price\r\n * @param {number} noi - Net Operating Income (annual)\r\n * @param {number} downPercent - Down payment percentage\r\n * @param {Object} options - Configuration options (uses config constants as defaults)\r\n * @returns {number} COCR percentage\r\n */\r\nexport function calculateCOCRAtPercent(askingPrice, noi, downPercent, options = {}) {\r\n const {\r\n dscrRate = DEFAULT_TIER.rate,\r\n dscrTerm = DEFAULT_TIER.amortization,\r\n } = options;\r\n\r\n try {\r\n const downDecimal = downPercent / 100;\r\n const cashInvested = askingPrice * downDecimal;\r\n \r\n // Fix financing structure: seller financing reduces available DSCR loan\r\n const dscrLoanAmount = askingPrice - cashInvested;\r\n \r\n const dscrPayment = calculatePMT(dscrLoanAmount, dscrRate, dscrTerm) * 12;\r\n \r\n const annualCashFlow = noi - dscrPayment;\r\n const cocr = (annualCashFlow / cashInvested) * 100;\r\n \r\n return cocr;\r\n } catch (error) {\r\n return 0;\r\n }\r\n}\r\n\r\n/**\r\n * Calculate STR (short-term rental) NOI - the single source of STR NOI math.\r\n *\r\n * Resolution order:\r\n * 1. apiResult type 'noi' -> value is already net, return as-is\r\n * 2. apiResult type 'gross' -> apply NOI margin (value * noiPercentage)\r\n * 3. no/invalid apiResult -> estimate from price (price * grossRate * noiPercentage)\r\n *\r\n * apiResult comes from api.archerjessop.com/str-revenue: { value, type }.\r\n * Pass null while that backend is not live (the price estimate is used).\r\n *\r\n * @param {number} askingPrice - Property asking price\r\n * @param {{value:number, type:'noi'|'gross'}|null} apiResult - STR revenue API result\r\n * @param {Object} options - Rate overrides (default to STR config constants)\r\n * @returns {number} Annual NOI (0 on invalid input)\r\n */\r\nexport function calculateSTRNOI(askingPrice, apiResult = null, options = {}) {\r\n const {\r\n grossRate = PROPERTY_TYPE_CONSTANTS.STR.ESTIMATED_GROSS_RATE,\r\n noiPercentage = PROPERTY_TYPE_CONSTANTS.STR.NOI_PERCENTAGE\r\n } = options;\r\n\r\n try {\r\n if (apiResult && Number.isFinite(apiResult.value) && apiResult.value >= 0) {\r\n if (apiResult.type === \"noi\") return apiResult.value;\r\n if (apiResult.type === \"gross\") return apiResult.value * noiPercentage;\r\n }\r\n\r\n if (!Number.isFinite(askingPrice) || askingPrice <= 0) return 0;\r\n return askingPrice * grossRate * noiPercentage;\r\n } catch (error) {\r\n return 0;\r\n }\r\n}\r\n\r\n/**\r\n * Calculate NOI based on property type\r\n * @param {number} askingPrice - Property asking price\r\n * @param {number} capRate - Cap rate as decimal (e.g., 0.08 for 8%)\r\n * @param {string} propertyType - Property type from PROPERTY_TYPES\r\n * @param {Object} options - Configuration options (uses config constants as defaults)\r\n * @returns {number} Calculated NOI\r\n */\r\nexport function calculateNOIByType(askingPrice, capRate, propertyType = PROPERTY_TYPES.MULTIFAMILY, options = {}) {\r\n const {\r\n strApiResult = null,\r\n strGrossIncomeMultiplier = PROPERTY_TYPE_CONSTANTS.STR.ESTIMATED_GROSS_RATE,\r\n strNoiPercentage = PROPERTY_TYPE_CONSTANTS.STR.NOI_PERCENTAGE,\r\n assistedIncomePerBedroom = PROPERTY_TYPE_CONSTANTS.ASSISTED_LIVING.INCOME_PER_BEDROOM_MONTHLY,\r\n bedroomCount = PROPERTY_TYPE_CONSTANTS.ASSISTED_LIVING.DEFAULT_BEDROOM_COUNT\r\n } = options;\r\n\r\n try {\r\n switch (propertyType.toLowerCase()) {\r\n case PROPERTY_TYPES.STR:\r\n return calculateSTRNOI(askingPrice, strApiResult, {\r\n grossRate: strGrossIncomeMultiplier,\r\n noiPercentage: strNoiPercentage\r\n });\r\n\r\n case PROPERTY_TYPES.ASSISTED_LIVING:\r\n return bedroomCount * assistedIncomePerBedroom * 12;\r\n \r\n case PROPERTY_TYPES.MULTIFAMILY:\r\n default:\r\n return askingPrice * capRate;\r\n }\r\n } catch (error) {\r\n return 0;\r\n }\r\n}\r\n\r\n/**\r\n * Calculate assignment fee\r\n * @param {number} askingPrice - Property asking price\r\n * @param {number} assignmentPercent - Assignment fee percentage (uses config default)\r\n * @returns {number} Assignment fee amount\r\n */\r\nexport function calculateAssignmentFee(askingPrice, assignmentPercent = BUSINESS_CONSTANTS.ASSIGNMENT_FEE_PERCENTAGE * 100) {\r\n try {\r\n return askingPrice * (assignmentPercent / 100);\r\n } catch (error) {\r\n return 0;\r\n }\r\n}\r\n\r\n/**\r\n * Calculate net to buyer\r\n * @param {number} askingPrice - Property asking price\r\n * @param {Object} options - Configuration options (uses config constants as defaults)\r\n * @returns {number} Net to buyer amount\r\n */\r\nexport function calculateNetToBuyer(askingPrice, options = {}) {\r\n const {\r\n buyerCostPercent = BUSINESS_CONSTANTS.NET_TO_BUYER_PERCENTAGE * 100,\r\n sellerCostAssignment = BUSINESS_CONSTANTS.ASSIGNMENT_FEE_PERCENTAGE * 100,\r\n sellerCostClosing = BUSINESS_CONSTANTS.CLOSING_COSTS_PERCENTAGE * 100,\r\n additionalCostRehab = BUSINESS_CONSTANTS.REHAB_RATE * 100,\r\n additionalCostFinancing = BUSINESS_CONSTANTS.HARD_MONEY_RATE * 100,\r\n dscrLtvPercent = FINANCIAL_CONSTANTS.DEFAULT_DSCR_PERCENTAGE * 100\r\n } = options;\r\n\r\n try {\r\n const dscrLoanAmount = askingPrice * (dscrLtvPercent / 100);\r\n \r\n return askingPrice * (buyerCostPercent / 100) - \r\n askingPrice * ((sellerCostAssignment + sellerCostClosing) / 100) - \r\n askingPrice * (additionalCostRehab / 100) - \r\n (additionalCostFinancing / 100) * (askingPrice - dscrLoanAmount);\r\n } catch (error) {\r\n return 0;\r\n }\r\n}\r\n\r\n/**\r\n * Calculate remaining loan balance at end of balloon period\r\n * @param {number} loanAmount - Initial loan amount\r\n * @param {number} interestRate - Annual interest rate as decimal (e.g., 0.075 for 7.5%)\r\n * @param {number} amortizationYears - Full amortization period in years\r\n * @param {number} balloonYears - Balloon period in years\r\n * @returns {number} Remaining balance at end of balloon period\r\n */\r\nexport function calculateBalloonBalance(loanAmount, interestRate, amortizationYears, balloonYears = FINANCIAL_CONSTANTS.DEFAULT_BALLOON_PERIOD_YEARS) {\r\n try {\r\n if (loanAmount <= 0 || interestRate < 0 || amortizationYears <= 0 || balloonYears <= 0) {\r\n return 0;\r\n }\r\n\r\n // If balloon period equals or exceeds amortization, loan is fully paid\r\n if (balloonYears >= amortizationYears) {\r\n return 0;\r\n }\r\n\r\n // Special handling for zero interest rate (simple linear paydown)\r\n if (interestRate === 0) {\r\n const totalPayments = amortizationYears * 12;\r\n const paymentsMade = balloonYears * 12;\r\n return loanAmount * (totalPayments - paymentsMade) / totalPayments;\r\n }\r\n\r\n const monthlyRate = interestRate / 12;\r\n const totalPayments = amortizationYears * 12;\r\n const balloonPayments = balloonYears * 12;\r\n\r\n // Calculate remaining balance using loan balance formula\r\n // Balance = P * [(1 + r)^n - (1 + r)^p] / [(1 + r)^n - 1]\r\n // Where P = principal, r = monthly rate, n = total payments, p = payments made\r\n \r\n const factor1 = Math.pow(1 + monthlyRate, totalPayments);\r\n const factor2 = Math.pow(1 + monthlyRate, balloonPayments);\r\n \r\n const remainingBalance = loanAmount * (factor1 - factor2) / (factor1 - 1);\r\n \r\n return Math.max(0, remainingBalance); // Ensure non-negative\r\n } catch (error) {\r\n return 0;\r\n }\r\n}\r\n\r\n/**\r\n * Calculate property value after appreciation period\r\n * @param {number} currentValue - Current property value\r\n * @param {number} appreciationRate - Annual appreciation rate as decimal\r\n * @param {number} years - Number of years\r\n * @returns {number} Appreciated property value\r\n */\r\nexport function calculateAppreciatedValue(currentValue, appreciationRate = FINANCIAL_CONSTANTS.APPRECIATION_RATE, years = FINANCIAL_CONSTANTS.DEFAULT_BALLOON_PERIOD_YEARS) {\r\n try {\r\n if (currentValue <= 0 || appreciationRate < 0 || years < 0) {\r\n return currentValue;\r\n }\r\n \r\n return currentValue * Math.pow(1 + appreciationRate, years);\r\n } catch (error) {\r\n return currentValue;\r\n }\r\n}\r\n\r\n/**\r\n * Calculate cash out amount after appreciation refinance\r\n * @param {number} originalPrice - Original purchase price\r\n * @param {number} dscrLoanAmount - Original DSCR loan amount \r\n * @param {number} sellerFiAmount - Original seller financing amount\r\n * @param {Object} options - Configuration options\r\n * @returns {number} Cash out amount (positive = cash out, negative = cash in)\r\n */\r\nexport function calculateCashOutAfterRefi(originalPrice, dscrLoanAmount, sellerFiAmount, options = {}) {\r\n const {\r\n appreciationRate = FINANCIAL_CONSTANTS.APPRECIATION_RATE,\r\n balloonYears = FINANCIAL_CONSTANTS.DEFAULT_BALLOON_PERIOD_YEARS,\r\n dscrRate = DEFAULT_TIER.rate,\r\n dscrTerm = DEFAULT_TIER.amortization,\r\n sellerFiTerm = FINANCIAL_CONSTANTS.SELLER_FI_AMORTIZATION,\r\n refiLtvPercent = 70 // 70% LTV on refi\r\n } = options;\r\n\r\n try {\r\n // Calculate appreciated property value\r\n const appreciatedValue = calculateAppreciatedValue(originalPrice, appreciationRate, balloonYears);\r\n \r\n // Calculate remaining balance on DSCR loan\r\n const dscrRemainingBalance = calculateBalloonBalance(dscrLoanAmount, dscrRate, dscrTerm, balloonYears);\r\n \r\n // Calculate remaining balance on seller financing (0% interest)\r\n const sellerFiRemainingBalance = calculateBalloonBalance(sellerFiAmount, FINANCIAL_CONSTANTS.SELLER_FI_INTEREST_RATE, sellerFiTerm, balloonYears);\r\n \r\n // Total remaining debt\r\n const totalRemainingDebt = dscrRemainingBalance + sellerFiRemainingBalance;\r\n \r\n // Calculate new loan amount at 70% LTV of appreciated value\r\n const newLoanAmount = appreciatedValue * (refiLtvPercent / 100);\r\n \r\n // Cash out = new loan - total remaining debt\r\n const cashOut = newLoanAmount - totalRemainingDebt;\r\n \r\n return cashOut;\r\n } catch (error) {\r\n return 0;\r\n }\r\n}\r\n\r\n// Cash Flow calculation (matching loopnet-analyzer exactly)\r\nexport function calculateCashFlow(monthlyNOI, dscrPayment, sfPayment) {\r\n return monthlyNOI - (dscrPayment + sfPayment);\r\n}\r\n\r\n/**\r\n * Calculate discount percentage from asking price and offered price\r\n * @param {number} askingPrice - Property asking price\r\n * @param {number} priceOffered - Offered price\r\n * @returns {number} Discount as decimal (positive = discount, negative = premium)\r\n */\r\nexport function calculateDiscountFromPrice(askingPrice, priceOffered) {\r\n if (!askingPrice || askingPrice <= 0) return 0;\r\n \r\n return (askingPrice - priceOffered) / askingPrice;\r\n}\r\n\r\n/**\r\n * Calculate price from asking price and discount percentage\r\n * @param {number} askingPrice - Property asking price \r\n * @param {number} discountPercent - Discount as decimal (positive = discount, negative = premium)\r\n * @returns {number} Calculated price\r\n */\r\nexport function calculatePriceFromDiscount(askingPrice, discountPercent) {\r\n if (!askingPrice || askingPrice <= 0) return 0;\r\n \r\n return askingPrice * (1 - discountPercent);\r\n}\r\n\r\nexport function safePercentage(value, fallback = 100) {\r\n return (value != null && !isNaN(value)) ? (value * 100) : fallback;\r\n}"],"names":["DEFAULT_TIER","FINANCIAL_CONSTANTS","INTEREST_RATE_TIERS","DEFAULT_INTEREST_RATE_TYPE","calculatePMT","principal","annualRate","years","monthlyRate","numPayments","Math","pow","calculateCOCR30","askingPrice","noi","cashInvested","dscrPayment","error","calculateCashFlowYield","monthlyCashFlow","purchasePrice","calculatePriceForCOCR","targetCOCR","options","downPercent","DEFAULT_DOWN_PAYMENT","dscrLtvPercent","DEFAULT_DSCR_PERCENTAGE","dscrRate","rate","dscrTerm","amortization","maxIterations","BUSINESS_CONSTANTS","MAX_ITERATIONS","tolerance","CALCULATION_TOLERANCE","targetPrice","iterations","currentCOCR","abs","adjustment","ADJUSTMENT_FACTOR","MAX_COCR15_PRICE_MULTIPLIER","CONSERVATIVE_COCR15_PRICE_MULTIPLIER","MINIMUM_COCR15_PRICE","calculateCOCRAtPercent","calculateSTRNOI","apiResult","grossRate","PROPERTY_TYPE_CONSTANTS","STR","ESTIMATED_GROSS_RATE","noiPercentage","NOI_PERCENTAGE","Number","isFinite","value","type","calculateNOIByType","capRate","propertyType","PROPERTY_TYPES","MULTIFAMILY","strApiResult","strGrossIncomeMultiplier","strNoiPercentage","assistedIncomePerBedroom","ASSISTED_LIVING","INCOME_PER_BEDROOM_MONTHLY","bedroomCount","DEFAULT_BEDROOM_COUNT","toLowerCase","calculateAssignmentFee","assignmentPercent","ASSIGNMENT_FEE_PERCENTAGE","calculateNetToBuyer","buyerCostPercent","NET_TO_BUYER_PERCENTAGE","sellerCostAssignment","sellerCostClosing","CLOSING_COSTS_PERCENTAGE","additionalCostRehab","REHAB_RATE","additionalCostFinancing","HARD_MONEY_RATE","calculateBalloonBalance","loanAmount","interestRate","amortizationYears","balloonYears","DEFAULT_BALLOON_PERIOD_YEARS","totalPayments","balloonPayments","factor1","remainingBalance","max","calculateAppreciatedValue","currentValue","appreciationRate","APPRECIATION_RATE","calculateCashOutAfterRefi","originalPrice","dscrLoanAmount","sellerFiAmount","sellerFiTerm","SELLER_FI_AMORTIZATION","refiLtvPercent","appreciatedValue","dscrRemainingBalance","sellerFiRemainingBalance","SELLER_FI_INTEREST_RATE","calculateCashFlow","monthlyNOI","sfPayment","calculateDiscountFromPrice","priceOffered","calculatePriceFromDiscount","discountPercent","safePercentage","fallback","isNaN"],"mappings":"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"}
|
package/dist/index.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export{calculateAppreciatedValue,calculateAssignmentFee,calculateBalloonBalance,calculateCOCR30,calculateCOCRAtPercent,calculateCashFlow,calculateCashFlowYield,calculateCashOutAfterRefi,calculateDiscountFromPrice,calculateNOIByType,calculateNetToBuyer,calculatePMT,calculatePriceForCOCR,calculatePriceFromDiscount,safePercentage}from"./financial/calculations.js";export{formatCurrency,formatPercentage,formatPriceValue}from"./financial/formatters.js";export{
|
|
1
|
+
export{calculateAppreciatedValue,calculateAssignmentFee,calculateBalloonBalance,calculateCOCR30,calculateCOCRAtPercent,calculateCashFlow,calculateCashFlowYield,calculateCashOutAfterRefi,calculateDiscountFromPrice,calculateNOIByType,calculateNetToBuyer,calculatePMT,calculatePriceForCOCR,calculatePriceFromDiscount,calculateSTRNOI,safePercentage}from"./financial/calculations.js";export{formatCurrency,formatPercentage,formatPriceValue}from"./financial/formatters.js";export{calculateOriginalPrice,convertCapRateToDecimal,createExportObjectCore,formatDownPaymentPercent}from"./export/export-logic.js";export{calculateDOM,formatDate}from"./date/utilities.js";export{calculateCursorPosition,extractNumericValue,filterNumericInput,formatInputDisplay,formatLiveInput,formatLiveNumber,parseNumericInput}from"./formatting/financial-formatting.js";export{normalizeWhitespace}from"./formatting/text.js";export{CALCULATION_TOLERANCE,DEFAULT_CAP_RATE,DEFAULT_DOWN_PAYMENT,DEFAULT_DSCR_PERCENTAGE,DEFAULT_EQUITY_ESTIMATE,DEFAULT_INTEREST_RATE_TYPE,FINANCIAL_CONSTANTS,INTEREST_RATE_TIERS,MAX_ITERATIONS,SELLER_FI_AMORTIZATION,SELLER_FI_CARRY,SELLER_FI_DOWN_PAYMENT,SELLER_FI_INTEREST_RATE,determineInterestRateType}from"./config/financial.js";export{ASSISTED_LIVING,MULTIFAMILY,PROPERTY_TYPES,PROPERTY_TYPE_CONSTANTS,STR}from"./config/property-types.js";export{ASSIGNMENT_FEE_PERCENTAGE,BUSINESS_CONSTANTS,BUYER_AGENT_COMMISSION,CLOSING_COSTS_PERCENTAGE,CONSERVATIVE_COCR15_PRICE_MULTIPLIER,HARD_MONEY_RATE,MAX_COCR15_PRICE_MULTIPLIER,MINIMUM_COCR15_PRICE,NET_TO_BUYER_PERCENTAGE,REHAB_RATE,SELLER_AGENT_COMMISSION}from"./config/business.js";export{lookupLOI}from"./services/loi-lookup.js";export{LOI_LOOKUP_CONFIG,LOI_SENT_STATUS,MATCH_TYPES}from"./config/loi-lookup.js";export{getEnvVar,isBrowserEnvironment,isNodeEnvironment}from"./environment/utilities.js";const e="./dist/styles/base.css";export{e as STYLES_PATH};
|
|
2
2
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sources":["../src/index.js"],"sourcesContent":["/**\r\n * @archerjessop/utilities\r\n * Shared utilities for ArcherJessop property analysis tools\r\n */\r\n\r\n// Financial calculations\r\nexport { \r\n calculateAppreciatedValue,\r\n calculateAssignmentFee,\r\n calculateBalloonBalance,\r\n calculateCashFlow,\r\n calculateCashFlowYield,\r\n calculateCashOutAfterRefi,\r\n calculateCOCR30, \r\n calculateCOCRAtPercent,\r\n calculateDiscountFromPrice,\r\n calculateNetToBuyer,\r\n calculateNOIByType,\r\n calculatePMT
|
|
1
|
+
{"version":3,"file":"index.js","sources":["../src/index.js"],"sourcesContent":["/**\r\n * @archerjessop/utilities\r\n * Shared utilities for ArcherJessop property analysis tools\r\n */\r\n\r\n// Financial calculations\r\nexport { \r\n calculateAppreciatedValue,\r\n calculateAssignmentFee,\r\n calculateBalloonBalance,\r\n calculateCashFlow,\r\n calculateCashFlowYield,\r\n calculateCashOutAfterRefi,\r\n calculateCOCR30, \r\n calculateCOCRAtPercent,\r\n calculateDiscountFromPrice,\r\n calculateNetToBuyer,\r\n calculateNOIByType,\r\n calculatePMT,\r\n calculatePriceForCOCR,\r\n calculatePriceFromDiscount,\r\n calculateSTRNOI,\r\n safePercentage,\r\n} from \"./financial/calculations.js\";\r\n\r\n// Financial formatters\r\nexport { formatCurrency, formatPriceValue, formatPercentage } from \"./financial/formatters.js\";\r\n\r\n// Export logic (pure export-object creation)\r\nexport {\r\n calculateOriginalPrice,\r\n convertCapRateToDecimal,\r\n createExportObjectCore,\r\n formatDownPaymentPercent,\r\n} from \"./export/export-logic.js\";\r\n\r\n// Date utilities\r\nexport { calculateDOM, formatDate } from \"./date/utilities.js\";\r\n\r\n// Formatting utilities\r\nexport { \r\n calculateCursorPosition,\r\n extractNumericValue,\r\n filterNumericInput,\r\n formatInputDisplay,\r\n formatLiveInput,\r\n formatLiveNumber,\r\n parseNumericInput\r\n} from \"./formatting/financial-formatting.js\";\r\n\r\n// Text formatting utilities\r\nexport { normalizeWhitespace } from \"./formatting/text.js\";\r\n\r\n// Configuration constants\r\nexport * from \"./config/financial.js\";\r\nexport * from \"./config/property-types.js\";\r\nexport * from \"./config/business.js\";\r\n\r\nexport const STYLES_PATH = \"./dist/styles/base.css\";\r\n\r\n// LOI Lookup service and config\r\nexport { lookupLOI } from \"./services/loi-lookup.js\";\r\nexport { LOI_LOOKUP_CONFIG, MATCH_TYPES, LOI_SENT_STATUS } from \"./config/loi-lookup.js\";\r\n\r\n// Environment utilities\r\nexport { \r\n getEnvVar, \r\n isNodeEnvironment, \r\n isBrowserEnvironment \r\n} from \"./environment/utilities.js\";"],"names":["STYLES_PATH"],"mappings":"yzDA0DY,MAACA,EAAc"}
|
package/package.json
CHANGED
|
@@ -1,18 +1,21 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@archerjessop/utilities",
|
|
3
|
-
"version": "7.
|
|
3
|
+
"version": "7.4.0",
|
|
4
4
|
"description": "Shared utilities for ArcherJessop property analysis tools",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
7
7
|
"exports": {
|
|
8
8
|
".": "./dist/index.js",
|
|
9
|
+
"./browser": "./dist/browser/index.js",
|
|
9
10
|
"./dist/styles/*.css": "./dist/styles/*.css"
|
|
10
11
|
},
|
|
11
12
|
"files": [
|
|
12
13
|
"dist",
|
|
13
14
|
"README.md"
|
|
14
15
|
],
|
|
15
|
-
"sideEffects":
|
|
16
|
+
"sideEffects": [
|
|
17
|
+
"./dist/browser/**"
|
|
18
|
+
],
|
|
16
19
|
"engines": {
|
|
17
20
|
"node": ">=16"
|
|
18
21
|
},
|
|
@@ -26,9 +29,9 @@
|
|
|
26
29
|
"prepublishOnly": "npm run clean && npm run build && npm run test",
|
|
27
30
|
"lint": "eslint src tests",
|
|
28
31
|
"lint:fix": "eslint src tests --fix",
|
|
29
|
-
"release:patch": "npm version patch &&
|
|
30
|
-
"release:minor": "npm version minor &&
|
|
31
|
-
"release:major": "npm version major &&
|
|
32
|
+
"release:patch": "npm test && npm version patch && npm publish --access public && git push && git push --tags",
|
|
33
|
+
"release:minor": "npm test && npm version minor && npm publish --access public && git push && git push --tags",
|
|
34
|
+
"release:major": "npm test && npm version major && npm publish --access public && git push && git push --tags"
|
|
32
35
|
},
|
|
33
36
|
"repository": {
|
|
34
37
|
"type": "git",
|
|
@@ -36,6 +39,9 @@
|
|
|
36
39
|
},
|
|
37
40
|
"author": "ArcherJessop",
|
|
38
41
|
"license": "MIT",
|
|
42
|
+
"dependencies": {
|
|
43
|
+
"@floating-ui/dom": "^1.7.4"
|
|
44
|
+
},
|
|
39
45
|
"devDependencies": {
|
|
40
46
|
"@rollup/plugin-node-resolve": "^15.2.3",
|
|
41
47
|
"@rollup/plugin-terser": "^0.4.4",
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"extractors.js","sources":["../../src/data/extractors.js"],"sourcesContent":["export function extractPhoneNumber() {\r\n const phoneElement = document.querySelector(\".phone-number\") ||\r\n document.querySelector(\"a[href^='tel:']\") ||\r\n document.querySelector(\".number\") ||\r\n document.querySelector(\"[class*='phone']\");\r\n \r\n if (phoneElement) {\r\n if (phoneElement.textContent && phoneElement.textContent.trim() !== \"Call\") {\r\n return phoneElement.textContent.trim();\r\n } else if (phoneElement.href) {\r\n // Extract from tel: link\r\n const telMatch = phoneElement.href.match(/tel:(.+)/);\r\n if (telMatch) {\r\n return telMatch[1];\r\n }\r\n }\r\n }\r\n \r\n // Fallback to text search with multiple patterns\r\n const pageText = document.body ? document.body.textContent || \"\" : \"\";\r\n const phoneMatch = pageText.match(/(\\+?1?\\s*\\(?[0-9]{3}\\)?[\\s.-]*[0-9]{3}[\\s.-]*[0-9]{4})/);\r\n if (phoneMatch) {\r\n return phoneMatch[1].trim();\r\n }\r\n \r\n return \"Not found\";\r\n}\r\n\r\nexport function extractBedrooms() {\r\n try {\r\n // Look for bedroom information in various places\r\n const bodyText = document.body?.textContent || \"\";\r\n \r\n // Common patterns for bedroom information\r\n const bedroomPatterns = [\r\n /(\\d+)\\s*bed/i,\r\n /(\\d+)\\s*bedroom/i,\r\n /beds?\\s*:\\s*(\\d+)/i,\r\n /bedrooms?\\s*:\\s*(\\d+)/i,\r\n /(\\d+)\\s*BR/i,\r\n /(\\d+)br/i\r\n ];\r\n \r\n for (const pattern of bedroomPatterns) {\r\n const match = bodyText.match(pattern);\r\n if (match) {\r\n const bedrooms = parseInt(match[1]);\r\n if (bedrooms > 0 && bedrooms < 100) { // Sanity check\r\n return bedrooms;\r\n }\r\n }\r\n }\r\n \r\n // Look in property details section specifically\r\n const propertyDetails = document.querySelector(\".property-details\") || \r\n document.querySelector(\"#PropertyDetails\") ||\r\n document.querySelector(\".details\");\r\n \r\n if (propertyDetails) {\r\n const detailsText = propertyDetails.textContent || \"\";\r\n for (const pattern of bedroomPatterns) {\r\n const match = detailsText.match(pattern);\r\n if (match) {\r\n const bedrooms = parseInt(match[1]);\r\n if (bedrooms > 0 && bedrooms < 100) {\r\n return bedrooms;\r\n }\r\n }\r\n }\r\n }\r\n \r\n // Default fallback\r\n return 10; // Default assumption for assisted living\r\n } catch (error) {\r\n return 10; // Default fallback\r\n }\r\n}\r\n\r\n"],"names":["extractPhoneNumber","phoneElement","document","querySelector","textContent","trim","href","telMatch","match","phoneMatch","body","extractBedrooms","bodyText","bedroomPatterns","pattern","bedrooms","parseInt","propertyDetails","detailsText","error"],"mappings":"AAAO,SAASA,qBACd,MAAMC,EAAeC,SAASC,cAAc,kBACxBD,SAASC,cAAc,oBACvBD,SAASC,cAAc,YACvBD,SAASC,cAAc,oBAE3C,GAAIF,EAAc,CAChB,GAAIA,EAAaG,aAAmD,SAApCH,EAAaG,YAAYC,OACvD,OAAOJ,EAAaG,YAAYC,OAC3B,GAAIJ,EAAaK,KAAM,CAE5B,MAAMC,EAAWN,EAAaK,KAAKE,MAAM,YACzC,GAAID,EACF,OAAOA,EAAS,EAEpB,CACF,CAGA,MACME,GADWP,SAASQ,MAAOR,SAASQ,KAAKN,aAAoB,IACvCI,MAAM,0DAClC,OAAIC,EACKA,EAAW,GAAGJ,OAGhB,WACT,CAEO,SAASM,kBACd,IAEE,MAAMC,EAAWV,SAASQ,MAAMN,aAAe,GAGzCS,EAAkB,CACtB,eACA,mBACA,qBACA,yBACA,cACA,YAGF,IAAK,MAAMC,KAAWD,EAAiB,CACrC,MAAML,EAAQI,EAASJ,MAAMM,GAC7B,GAAIN,EAAO,CACT,MAAMO,EAAWC,SAASR,EAAM,IAChC,GAAIO,EAAW,GAAKA,EAAW,IAC7B,OAAOA,CAEX,CACF,CAGA,MAAME,EAAkBf,SAASC,cAAc,sBACxBD,SAASC,cAAc,qBACvBD,SAASC,cAAc,YAE9C,GAAIc,EAAiB,CACnB,MAAMC,EAAcD,EAAgBb,aAAe,GACnD,IAAK,MAAMU,KAAWD,EAAiB,CACrC,MAAML,EAAQU,EAAYV,MAAMM,GAChC,GAAIN,EAAO,CACT,MAAMO,EAAWC,SAASR,EAAM,IAChC,GAAIO,EAAW,GAAKA,EAAW,IAC7B,OAAOA,CAEX,CACF,CACF,CAGA,OAAO,EACT,CAAE,MAAOI,GACP,OAAO,EACT,CACF"}
|
|
File without changes
|