PageSourceSearch

https://tetherx.io/assets/controllers/roi_calculator_controller-74bdf1a8.js

js tetherx.io collected 2026-10-02 00:12:12 UTC 4,468 bytes, 109 lines download raw bytes

1import { Controller } from "@hotwired/stimulus"
2import { localeCurrency } from "currency"
3
4// Connects to data-controller="roi-calculator"
5//
6// Live monthly-recurring-revenue calculator for the integrators page. Three
7// inputs (cameras per site, number of sites, cloud recording cost per camera)
8// plus a set of additional-service checkboxes feed a single annual-revenue
9// figure, displayed as monthly / annual / 7-year headlines.
10//
11// Pricing model per checkbox (exactly one attribute each):
12//   data-percent-uplift          -> adds N% of base annual revenue
13//   data-monthly-per-camera-gbp  -> flat GBP/camera/month
14//   data-monthly-per-site-gbp    -> flat GBP/site/month
15//
16// Currency is centralised in currency.js (localeCurrency); an AU visitor sees
17// A$ figures using the shared rate table, never a hardcoded symbol.
18//
19// connect()/disconnect() replace the old turbo:load inline script - Stimulus
20// re-binds on every Turbo navigation, so no accumulating listeners and the
21// calculator recomputes on every page entry.
22export default class extends Controller {
23  static targets = [
24    "camerasPerSite",
25    "numberOfSites",
26    "cloudRecordingCost",
27    "addon",
28    "monthlyResult",
29    "annualResult",
30    "sevenYearResult"
31  ]
32
33  connect() {
34    this.currency = localeCurrency()
35    this.convertCloudCostOptions()
36    this.updateCalculation()
37  }
38
39  // GBP -> locale currency, rounded up to the next whole unit (matches
40  // convertGbp in currency.js and the prior inline script).
41  toLocale(gbp) {
42    return Math.ceil(gbp * this.currency.rate)
43  }
44
45  // Compact figure formatting: 1.2b / 3.4m / 56k / 1,234.
46  formatAmount(value) {
47    if (value >= 1e9) return `${(value / 1e9).toFixed(1)}b`
48    if (value >= 1e6) return `${(value / 1e6).toFixed(1)}m`
49    if (value >= 1e5) return `${(value / 1e3).toFixed(0)}k`
50    return value.toLocaleString()
51  }
52
53  // Rewrite the cloud-recording-cost option labels into locale currency,
54  // preserving any "(Platform Only)" suffix, and push the new label into the
55  // TomSelect clone if one is mounted.
56  convertCloudCostOptions() {
57    const select = this.cloudRecordingCostTarget
58    Array.from(select.options).forEach((opt) => {
59      const gbpMonthly = parseInt(opt.value)
60      const suffix = opt.textContent.includes("(")
61        ? ` ${opt.textContent.slice(opt.textContent.indexOf("("))}`
62        : ""
63      const label = `${this.currency.symbol}${this.toLocale(gbpMonthly).toLocaleString()}/month${suffix}`
64      opt.textContent = label
65      if (select.tomselect) {
66        select.tomselect.updateOption(opt.value, { value: opt.value, text: label })
67      }
68    })
69  }
70
71  updateCalculation() {
72    const c = this.currency.symbol
73    const camerasPerSite = parseInt(this.camerasPerSiteTarget.value)
74    const numberOfSites = parseInt(this.numberOfSitesTarget.value)
75    const monthlyCostPerCamera = parseInt(this.cloudRecordingCostTarget.value)
76    const annualCostPerCamera = monthlyCostPerCamera * 12
77    const totalCameras = camerasPerSite * numberOfSites
78
79    let percentUpliftTotal = 0
80    let perCameraMonthlyGbp = 0
81    let perSiteMonthlyGbp = 0
82    this.addonTargets.forEach((cb) => {
83      if (!cb.checked) return
84      const pct = parseFloat(cb.dataset.percentUplift)
85      const cam = parseFloat(cb.dataset.monthlyPerCameraGbp)
86      const site = parseFloat(cb.dataset.monthlyPerSiteGbp)
87      if (!Number.isNaN(pct) && pct > 0) percentUpliftTotal += pct
88      if (!Number.isNaN(cam) && cam > 0) perCameraMonthlyGbp += cam
89      if (!Number.isNaN(site) && site > 0) perSiteMonthlyGbp += site
90    })
91    const serviceMultiplier = 1 + percentUpliftTotal
92
93    const baseAnnualRevenue = totalCameras * annualCostPerCamera
94    const perCameraAnnualGbp = totalCameras * perCameraMonthlyGbp * 12
95    const perSiteAnnualGbp = numberOfSites * perSiteMonthlyGbp * 12
96    const annualRevenue = Math.round(
97      baseAnnualRevenue * serviceMultiplier + perCameraAnnualGbp + perSiteAnnualGbp
98    )
99
100    this.annualResultTarget.textContent = `${c}
100${this.formatAmount(this.toLocale(annualRevenue))}`
101    this.monthlyResultTarget.textContent = `${c}${this.formatAmount(this.toLocale(Math.round(annualRevenue / 12)))}`
102    this.sevenYearResultTarget.textContent = `${c}${this.formatAmount(this.toLocale(annualRevenue * 7))}`
103
104    const panel = this.annualResultTarget.closest(".results-panel")
105    if (panel && panel.style.visibility === "hidden") {
106      panel.style.visibility = "visible"
107    }
108  }
109}

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.