@hailbytes/security-roi-calculator 1.0.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/CHANGELOG.md +22 -0
- package/LICENSE +374 -0
- package/README.md +275 -0
- package/hailbytes-roi-calculator.js +999 -0
- package/index.d.ts +63 -0
- package/package.json +59 -0
|
@@ -0,0 +1,999 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HailBytes Security ROI Calculator
|
|
3
|
+
* Zero-dependency Web Component — no build step required.
|
|
4
|
+
* Usage: <hailbytes-roi-calculator></hailbytes-roi-calculator>
|
|
5
|
+
*
|
|
6
|
+
* @license MPL-2.0
|
|
7
|
+
* @see https://hailbytes.com
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
// ─── Calculation engine ────────────────────────────────────────────────────────
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* @typedef {Object} ROIInputs
|
|
14
|
+
* @property {number} employeeCount
|
|
15
|
+
* @property {number} avgSalary
|
|
16
|
+
* @property {number} incidentsPerYear
|
|
17
|
+
* @property {number} avgIncidentCost
|
|
18
|
+
* @property {number} clickRateBefore - % (0-100)
|
|
19
|
+
* @property {number} expectedReduction - % (0-100) reduction in click rate
|
|
20
|
+
* @property {number} trainingHoursPerYear
|
|
21
|
+
* @property {number} platformLicensing - $ per year total
|
|
22
|
+
* @property {number} implementationHours
|
|
23
|
+
* @property {number} hourlyRate
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Core ROI calculation — pure function, no DOM.
|
|
28
|
+
* @param {ROIInputs} inputs
|
|
29
|
+
* @returns {Object} full result object
|
|
30
|
+
*/
|
|
31
|
+
function calculateROI(inputs) {
|
|
32
|
+
const {
|
|
33
|
+
employeeCount,
|
|
34
|
+
avgSalary,
|
|
35
|
+
incidentsPerYear,
|
|
36
|
+
avgIncidentCost,
|
|
37
|
+
clickRateBefore,
|
|
38
|
+
expectedReduction,
|
|
39
|
+
trainingHoursPerYear,
|
|
40
|
+
platformLicensing,
|
|
41
|
+
implementationHours,
|
|
42
|
+
hourlyRate,
|
|
43
|
+
} = inputs;
|
|
44
|
+
|
|
45
|
+
// Reduced click rate after training
|
|
46
|
+
const clickRateReduction = (clickRateBefore * expectedReduction) / 100;
|
|
47
|
+
const clickRateAfter = Math.max(0, clickRateBefore - clickRateReduction);
|
|
48
|
+
|
|
49
|
+
// Annual risk reduction — incidents avoided × cost per incident × reduction fraction
|
|
50
|
+
const annualRiskReduction = incidentsPerYear * avgIncidentCost * (clickRateReduction / 100);
|
|
51
|
+
|
|
52
|
+
// Productivity cost of training time
|
|
53
|
+
const hourlyWage = avgSalary / 2080; // 2080 work hours/year
|
|
54
|
+
const productivityCost = employeeCount * trainingHoursPerYear * hourlyWage;
|
|
55
|
+
|
|
56
|
+
// Total training cost
|
|
57
|
+
const implementationCost = implementationHours * hourlyRate;
|
|
58
|
+
const totalTrainingCost = platformLicensing + implementationCost + productivityCost;
|
|
59
|
+
|
|
60
|
+
// Net benefit & ROI
|
|
61
|
+
const netBenefit = annualRiskReduction - totalTrainingCost;
|
|
62
|
+
const roi = totalTrainingCost > 0 ? (netBenefit / totalTrainingCost) * 100 : 0;
|
|
63
|
+
|
|
64
|
+
// Payback period in months (how long until cumulative benefit covers cost)
|
|
65
|
+
const monthlyBenefit = annualRiskReduction / 12;
|
|
66
|
+
const paybackMonths = monthlyBenefit > 0 ? totalTrainingCost / monthlyBenefit : Infinity;
|
|
67
|
+
|
|
68
|
+
// Prevented incidents
|
|
69
|
+
const preventedIncidents = incidentsPerYear * (clickRateReduction / 100);
|
|
70
|
+
|
|
71
|
+
return {
|
|
72
|
+
// Inputs (echoed back)
|
|
73
|
+
inputs,
|
|
74
|
+
// Key metrics
|
|
75
|
+
annualRiskReduction,
|
|
76
|
+
totalTrainingCost,
|
|
77
|
+
platformLicensing,
|
|
78
|
+
implementationCost,
|
|
79
|
+
productivityCost,
|
|
80
|
+
netBenefit,
|
|
81
|
+
roi,
|
|
82
|
+
paybackMonths,
|
|
83
|
+
clickRateBefore,
|
|
84
|
+
clickRateAfter,
|
|
85
|
+
clickRateReduction,
|
|
86
|
+
preventedIncidents,
|
|
87
|
+
// Convenience
|
|
88
|
+
isPositiveROI: roi > 0,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// ─── Helpers ─────────────────────────────────────────────────────────────────
|
|
93
|
+
|
|
94
|
+
function fmt$(n) {
|
|
95
|
+
if (!isFinite(n)) return '—';
|
|
96
|
+
return '$' + Math.round(n).toLocaleString('en-US');
|
|
97
|
+
}
|
|
98
|
+
function fmtPct(n) {
|
|
99
|
+
if (!isFinite(n)) return '—';
|
|
100
|
+
return Math.round(n).toLocaleString('en-US') + '%';
|
|
101
|
+
}
|
|
102
|
+
function fmtNum(n, decimals = 1) {
|
|
103
|
+
if (!isFinite(n)) return '—';
|
|
104
|
+
return n.toFixed(decimals);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// ─── Styles ───────────────────────────────────────────────────────────────────
|
|
108
|
+
|
|
109
|
+
const STYLES = `
|
|
110
|
+
:host {
|
|
111
|
+
display: block;
|
|
112
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
113
|
+
line-height: 1.5;
|
|
114
|
+
box-sizing: border-box;
|
|
115
|
+
}
|
|
116
|
+
*, *::before, *::after { box-sizing: inherit; }
|
|
117
|
+
|
|
118
|
+
:host([branding="off"]) .hb-branding { display: none; }
|
|
119
|
+
|
|
120
|
+
/* ── Themes ── */
|
|
121
|
+
:host([theme="dark"]), :host(.dark) {
|
|
122
|
+
--bg: #1a1a2e;
|
|
123
|
+
--bg-card: #16213e;
|
|
124
|
+
--bg-input: #0f3460;
|
|
125
|
+
--border: #2d4a7a;
|
|
126
|
+
--text: #e0e0e0;
|
|
127
|
+
--muted: #8892a4;
|
|
128
|
+
--accent: #ff6b35;
|
|
129
|
+
--accent-dim: rgba(255,107,53,.12);
|
|
130
|
+
--green: #2ed573;
|
|
131
|
+
--red: #ff4757;
|
|
132
|
+
--shadow: 0 4px 24px rgba(0,0,0,.45);
|
|
133
|
+
--tab-bg: #0f3460;
|
|
134
|
+
--tab-active: #ff6b35;
|
|
135
|
+
}
|
|
136
|
+
:host, :host([theme="light"]) {
|
|
137
|
+
--bg: #f5f7fa;
|
|
138
|
+
--bg-card: #ffffff;
|
|
139
|
+
--bg-input: #ffffff;
|
|
140
|
+
--border: #dde2ec;
|
|
141
|
+
--text: #1a1a2e;
|
|
142
|
+
--muted: #6b7280;
|
|
143
|
+
--accent: #ff6b35;
|
|
144
|
+
--accent-dim: rgba(255,107,53,.08);
|
|
145
|
+
--green: #16a34a;
|
|
146
|
+
--red: #dc2626;
|
|
147
|
+
--shadow: 0 4px 24px rgba(0,0,0,.08);
|
|
148
|
+
--tab-bg: #f1f3f8;
|
|
149
|
+
--tab-active: #ff6b35;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
.wrapper {
|
|
153
|
+
background: var(--bg);
|
|
154
|
+
color: var(--text);
|
|
155
|
+
border-radius: 14px;
|
|
156
|
+
overflow: hidden;
|
|
157
|
+
box-shadow: var(--shadow);
|
|
158
|
+
max-width: 680px;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/* ── Header ── */
|
|
162
|
+
.header {
|
|
163
|
+
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
|
|
164
|
+
padding: 1.5rem 1.75rem 1.25rem;
|
|
165
|
+
display: flex; align-items: center; gap: .85rem;
|
|
166
|
+
border-bottom: 2px solid var(--accent);
|
|
167
|
+
}
|
|
168
|
+
.logo-mark {
|
|
169
|
+
width: 40px; height: 40px; flex-shrink: 0;
|
|
170
|
+
background: var(--accent);
|
|
171
|
+
border-radius: 9px;
|
|
172
|
+
display: flex; align-items: center; justify-content: center;
|
|
173
|
+
font-size: 1.2rem; font-weight: 800; color: #fff;
|
|
174
|
+
}
|
|
175
|
+
.header-text h2 { margin: 0; font-size: 1.15rem; font-weight: 700; color: #fff; }
|
|
176
|
+
.header-text p { margin: 0; font-size: .8rem; color: #8892a4; }
|
|
177
|
+
|
|
178
|
+
/* ── Tab bar ── */
|
|
179
|
+
.tab-bar {
|
|
180
|
+
display: flex;
|
|
181
|
+
background: var(--tab-bg);
|
|
182
|
+
border-bottom: 1px solid var(--border);
|
|
183
|
+
overflow-x: auto;
|
|
184
|
+
}
|
|
185
|
+
.tab-btn {
|
|
186
|
+
flex: 1;
|
|
187
|
+
min-width: 90px;
|
|
188
|
+
padding: .7rem .5rem;
|
|
189
|
+
background: none;
|
|
190
|
+
border: none;
|
|
191
|
+
border-bottom: 3px solid transparent;
|
|
192
|
+
cursor: pointer;
|
|
193
|
+
font-size: .82rem;
|
|
194
|
+
font-weight: 600;
|
|
195
|
+
color: var(--muted);
|
|
196
|
+
transition: all .2s;
|
|
197
|
+
white-space: nowrap;
|
|
198
|
+
}
|
|
199
|
+
.tab-btn:hover { color: var(--text); background: rgba(128,128,128,.07); }
|
|
200
|
+
.tab-btn.active {
|
|
201
|
+
color: var(--accent);
|
|
202
|
+
border-bottom-color: var(--accent);
|
|
203
|
+
background: var(--bg-card);
|
|
204
|
+
}
|
|
205
|
+
.tab-step {
|
|
206
|
+
display: inline-block;
|
|
207
|
+
width: 20px; height: 20px;
|
|
208
|
+
border-radius: 50%;
|
|
209
|
+
background: var(--border);
|
|
210
|
+
color: var(--muted);
|
|
211
|
+
font-size: .7rem;
|
|
212
|
+
font-weight: 700;
|
|
213
|
+
line-height: 20px;
|
|
214
|
+
text-align: center;
|
|
215
|
+
margin-right: .3rem;
|
|
216
|
+
vertical-align: middle;
|
|
217
|
+
}
|
|
218
|
+
.tab-btn.active .tab-step {
|
|
219
|
+
background: var(--accent);
|
|
220
|
+
color: #fff;
|
|
221
|
+
}
|
|
222
|
+
.tab-btn.done .tab-step {
|
|
223
|
+
background: var(--green);
|
|
224
|
+
color: #fff;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/* ── Tab content ── */
|
|
228
|
+
.tab-panel {
|
|
229
|
+
display: none;
|
|
230
|
+
padding: 1.5rem 1.75rem;
|
|
231
|
+
background: var(--bg-card);
|
|
232
|
+
min-height: 260px;
|
|
233
|
+
}
|
|
234
|
+
.tab-panel.active { display: block; }
|
|
235
|
+
|
|
236
|
+
.panel-title {
|
|
237
|
+
font-size: 1rem;
|
|
238
|
+
font-weight: 700;
|
|
239
|
+
margin-bottom: 1.1rem;
|
|
240
|
+
color: var(--text);
|
|
241
|
+
display: flex; align-items: center; gap: .5rem;
|
|
242
|
+
}
|
|
243
|
+
.panel-title-icon { font-size: 1.2rem; }
|
|
244
|
+
|
|
245
|
+
/* ── Form fields ── */
|
|
246
|
+
.field-grid {
|
|
247
|
+
display: grid;
|
|
248
|
+
grid-template-columns: 1fr 1fr;
|
|
249
|
+
gap: 1rem;
|
|
250
|
+
}
|
|
251
|
+
@media (max-width: 420px) { .field-grid { grid-template-columns: 1fr; } }
|
|
252
|
+
|
|
253
|
+
.field { display: flex; flex-direction: column; gap: .3rem; }
|
|
254
|
+
.field.full { grid-column: 1 / -1; }
|
|
255
|
+
|
|
256
|
+
label {
|
|
257
|
+
font-size: .82rem;
|
|
258
|
+
font-weight: 600;
|
|
259
|
+
color: var(--muted);
|
|
260
|
+
}
|
|
261
|
+
label .hint {
|
|
262
|
+
font-weight: 400;
|
|
263
|
+
font-style: italic;
|
|
264
|
+
opacity: .75;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
input[type="number"], input[type="text"] {
|
|
268
|
+
padding: .6rem .8rem;
|
|
269
|
+
background: var(--bg-input);
|
|
270
|
+
border: 1.5px solid var(--border);
|
|
271
|
+
border-radius: 7px;
|
|
272
|
+
color: var(--text);
|
|
273
|
+
font-size: .95rem;
|
|
274
|
+
outline: none;
|
|
275
|
+
transition: border-color .2s;
|
|
276
|
+
width: 100%;
|
|
277
|
+
}
|
|
278
|
+
input:focus {
|
|
279
|
+
border-color: var(--accent);
|
|
280
|
+
box-shadow: 0 0 0 3px rgba(255,107,53,.15);
|
|
281
|
+
}
|
|
282
|
+
input.invalid { border-color: var(--red); }
|
|
283
|
+
|
|
284
|
+
.field-error {
|
|
285
|
+
font-size: .75rem;
|
|
286
|
+
color: var(--red);
|
|
287
|
+
min-height: 1rem;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
.auto-badge {
|
|
291
|
+
font-size: .7rem;
|
|
292
|
+
background: var(--accent-dim);
|
|
293
|
+
color: var(--accent);
|
|
294
|
+
border: 1px solid rgba(255,107,53,.25);
|
|
295
|
+
border-radius: 10px;
|
|
296
|
+
padding: .1rem .45rem;
|
|
297
|
+
margin-left: .3rem;
|
|
298
|
+
vertical-align: middle;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/* ── Navigation buttons ── */
|
|
302
|
+
.nav-row {
|
|
303
|
+
display: flex;
|
|
304
|
+
justify-content: space-between;
|
|
305
|
+
align-items: center;
|
|
306
|
+
padding: 1rem 1.75rem 1.25rem;
|
|
307
|
+
background: var(--bg-card);
|
|
308
|
+
border-top: 1px solid var(--border);
|
|
309
|
+
gap: .5rem;
|
|
310
|
+
}
|
|
311
|
+
.btn {
|
|
312
|
+
padding: .6rem 1.4rem;
|
|
313
|
+
border-radius: 7px;
|
|
314
|
+
border: none;
|
|
315
|
+
font-size: .88rem;
|
|
316
|
+
font-weight: 700;
|
|
317
|
+
cursor: pointer;
|
|
318
|
+
transition: all .15s;
|
|
319
|
+
display: flex; align-items: center; gap: .4rem;
|
|
320
|
+
}
|
|
321
|
+
.btn-primary {
|
|
322
|
+
background: var(--accent);
|
|
323
|
+
color: #fff;
|
|
324
|
+
}
|
|
325
|
+
.btn-primary:hover { background: #e55a28; transform: translateY(-1px); }
|
|
326
|
+
.btn-secondary {
|
|
327
|
+
background: transparent;
|
|
328
|
+
color: var(--muted);
|
|
329
|
+
border: 1.5px solid var(--border);
|
|
330
|
+
}
|
|
331
|
+
.btn-secondary:hover { color: var(--text); border-color: var(--text); }
|
|
332
|
+
.btn:disabled { opacity: .4; cursor: not-allowed; transform: none !important; }
|
|
333
|
+
|
|
334
|
+
.progress-text {
|
|
335
|
+
font-size: .78rem;
|
|
336
|
+
color: var(--muted);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/* ── Results panel ── */
|
|
340
|
+
.metrics-grid {
|
|
341
|
+
display: grid;
|
|
342
|
+
grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
|
|
343
|
+
gap: .85rem;
|
|
344
|
+
margin-bottom: 1.5rem;
|
|
345
|
+
}
|
|
346
|
+
.metric-card {
|
|
347
|
+
background: var(--bg);
|
|
348
|
+
border: 1px solid var(--border);
|
|
349
|
+
border-radius: 10px;
|
|
350
|
+
padding: 1rem;
|
|
351
|
+
text-align: center;
|
|
352
|
+
}
|
|
353
|
+
.metric-card.highlight {
|
|
354
|
+
border-color: var(--accent);
|
|
355
|
+
background: var(--accent-dim);
|
|
356
|
+
}
|
|
357
|
+
.metric-card.positive { border-color: var(--green); }
|
|
358
|
+
.metric-card.negative { border-color: var(--red); }
|
|
359
|
+
|
|
360
|
+
.metric-icon { font-size: 1.4rem; margin-bottom: .35rem; }
|
|
361
|
+
.metric-label {
|
|
362
|
+
font-size: .72rem;
|
|
363
|
+
text-transform: uppercase;
|
|
364
|
+
letter-spacing: .04em;
|
|
365
|
+
color: var(--muted);
|
|
366
|
+
margin-bottom: .25rem;
|
|
367
|
+
}
|
|
368
|
+
.metric-value {
|
|
369
|
+
font-size: 1.35rem;
|
|
370
|
+
font-weight: 800;
|
|
371
|
+
color: var(--text);
|
|
372
|
+
line-height: 1.1;
|
|
373
|
+
}
|
|
374
|
+
.metric-value.green { color: var(--green); }
|
|
375
|
+
.metric-value.red { color: var(--red); }
|
|
376
|
+
.metric-value.accent { color: var(--accent); }
|
|
377
|
+
|
|
378
|
+
/* ── Bar chart ── */
|
|
379
|
+
.chart-section { margin-bottom: 1.5rem; }
|
|
380
|
+
.chart-title {
|
|
381
|
+
font-size: .82rem;
|
|
382
|
+
font-weight: 700;
|
|
383
|
+
color: var(--muted);
|
|
384
|
+
text-transform: uppercase;
|
|
385
|
+
letter-spacing: .04em;
|
|
386
|
+
margin-bottom: .75rem;
|
|
387
|
+
}
|
|
388
|
+
.bar-chart { display: flex; flex-direction: column; gap: .6rem; }
|
|
389
|
+
.bar-row { display: flex; flex-direction: column; gap: .2rem; }
|
|
390
|
+
.bar-label {
|
|
391
|
+
display: flex; justify-content: space-between;
|
|
392
|
+
font-size: .8rem; color: var(--muted);
|
|
393
|
+
}
|
|
394
|
+
.bar-label strong { color: var(--text); }
|
|
395
|
+
.bar-track {
|
|
396
|
+
height: 22px;
|
|
397
|
+
background: var(--bg);
|
|
398
|
+
border: 1px solid var(--border);
|
|
399
|
+
border-radius: 5px;
|
|
400
|
+
overflow: hidden;
|
|
401
|
+
}
|
|
402
|
+
.bar-fill {
|
|
403
|
+
height: 100%;
|
|
404
|
+
border-radius: 5px;
|
|
405
|
+
transition: width .6s ease;
|
|
406
|
+
min-width: 2px;
|
|
407
|
+
}
|
|
408
|
+
.bar-fill.risk-reduction { background: var(--green); }
|
|
409
|
+
.bar-fill.training-cost { background: var(--accent); }
|
|
410
|
+
.bar-fill.net-benefit { background: #1e90ff; }
|
|
411
|
+
|
|
412
|
+
/* ── Breakdown table ── */
|
|
413
|
+
.breakdown-section { margin-bottom: 1.25rem; }
|
|
414
|
+
.breakdown-title {
|
|
415
|
+
font-size: .82rem;
|
|
416
|
+
font-weight: 700;
|
|
417
|
+
color: var(--muted);
|
|
418
|
+
text-transform: uppercase;
|
|
419
|
+
letter-spacing: .04em;
|
|
420
|
+
margin-bottom: .6rem;
|
|
421
|
+
}
|
|
422
|
+
.breakdown-rows { display: flex; flex-direction: column; gap: 0; }
|
|
423
|
+
.breakdown-row {
|
|
424
|
+
display: flex;
|
|
425
|
+
justify-content: space-between;
|
|
426
|
+
align-items: center;
|
|
427
|
+
padding: .45rem .7rem;
|
|
428
|
+
font-size: .83rem;
|
|
429
|
+
border-bottom: 1px solid var(--border);
|
|
430
|
+
}
|
|
431
|
+
.breakdown-row:last-child { border-bottom: none; }
|
|
432
|
+
.breakdown-row.total {
|
|
433
|
+
font-weight: 700;
|
|
434
|
+
font-size: .88rem;
|
|
435
|
+
background: var(--accent-dim);
|
|
436
|
+
border-radius: 6px;
|
|
437
|
+
margin-top: .3rem;
|
|
438
|
+
border-bottom: none;
|
|
439
|
+
}
|
|
440
|
+
.breakdown-key { color: var(--muted); }
|
|
441
|
+
.breakdown-val { font-weight: 600; color: var(--text); }
|
|
442
|
+
|
|
443
|
+
/* ── ROI callout ── */
|
|
444
|
+
.roi-callout {
|
|
445
|
+
background: linear-gradient(135deg, #1a1a2e, #0f3460);
|
|
446
|
+
border: 1px solid var(--accent);
|
|
447
|
+
border-radius: 10px;
|
|
448
|
+
padding: 1.1rem 1.25rem;
|
|
449
|
+
display: flex;
|
|
450
|
+
align-items: center;
|
|
451
|
+
gap: 1rem;
|
|
452
|
+
margin-bottom: 1.25rem;
|
|
453
|
+
}
|
|
454
|
+
.roi-callout-icon { font-size: 2rem; }
|
|
455
|
+
.roi-callout-text .big {
|
|
456
|
+
font-size: 1.5rem;
|
|
457
|
+
font-weight: 800;
|
|
458
|
+
color: var(--green);
|
|
459
|
+
}
|
|
460
|
+
.roi-callout-text .big.neg { color: var(--red); }
|
|
461
|
+
.roi-callout-text .sub { font-size: .82rem; color: #8892a4; }
|
|
462
|
+
|
|
463
|
+
/* ── CTA row ── */
|
|
464
|
+
.results-cta {
|
|
465
|
+
text-align: center;
|
|
466
|
+
padding-top: .5rem;
|
|
467
|
+
}
|
|
468
|
+
.results-cta p { font-size: .85rem; color: var(--muted); margin-bottom: .75rem; }
|
|
469
|
+
.cta-link {
|
|
470
|
+
display: inline-flex; align-items: center; gap: .4rem;
|
|
471
|
+
background: var(--accent);
|
|
472
|
+
color: #fff;
|
|
473
|
+
text-decoration: none;
|
|
474
|
+
padding: .6rem 1.4rem;
|
|
475
|
+
border-radius: 7px;
|
|
476
|
+
font-weight: 700;
|
|
477
|
+
font-size: .88rem;
|
|
478
|
+
transition: background .15s;
|
|
479
|
+
}
|
|
480
|
+
.cta-link:hover { background: #e55a28; }
|
|
481
|
+
|
|
482
|
+
/* ── Empty state ── */
|
|
483
|
+
.empty-results {
|
|
484
|
+
text-align: center;
|
|
485
|
+
padding: 2rem 1rem;
|
|
486
|
+
color: var(--muted);
|
|
487
|
+
}
|
|
488
|
+
.empty-results .icon { font-size: 2.5rem; margin-bottom: .75rem; }
|
|
489
|
+
`;
|
|
490
|
+
|
|
491
|
+
// ─── Template builder ───────────────────────────────────────────────────────
|
|
492
|
+
|
|
493
|
+
function buildTemplate() {
|
|
494
|
+
return `
|
|
495
|
+
<style>${STYLES}</style>
|
|
496
|
+
<div class="wrapper" part="wrapper">
|
|
497
|
+
|
|
498
|
+
<div class="header">
|
|
499
|
+
<div class="logo-mark">📊</div>
|
|
500
|
+
<div class="header-text">
|
|
501
|
+
<h2>Security Awareness ROI Calculator</h2>
|
|
502
|
+
<p class="hb-branding">by <a href="https://hailbytes.com" target="_blank" rel="noopener" style="color:#ff6b35;text-decoration:none">HailBytes</a> — estimate the ROI of security training</p>
|
|
503
|
+
</div>
|
|
504
|
+
</div>
|
|
505
|
+
|
|
506
|
+
<!-- Tab bar -->
|
|
507
|
+
<div class="tab-bar" role="tablist">
|
|
508
|
+
<button class="tab-btn active" data-tab="0" role="tab">
|
|
509
|
+
<span class="tab-step">1</span>Baseline
|
|
510
|
+
</button>
|
|
511
|
+
<button class="tab-btn" data-tab="1" role="tab">
|
|
512
|
+
<span class="tab-step">2</span>Training
|
|
513
|
+
</button>
|
|
514
|
+
<button class="tab-btn" data-tab="2" role="tab">
|
|
515
|
+
<span class="tab-step">3</span>Costs
|
|
516
|
+
</button>
|
|
517
|
+
<button class="tab-btn" data-tab="3" role="tab">
|
|
518
|
+
<span class="tab-step">4</span>Results
|
|
519
|
+
</button>
|
|
520
|
+
</div>
|
|
521
|
+
|
|
522
|
+
<!-- Panel 0: Baseline -->
|
|
523
|
+
<div class="tab-panel active" data-panel="0">
|
|
524
|
+
<div class="panel-title"><span class="panel-title-icon">🏢</span> Organization Baseline</div>
|
|
525
|
+
<div class="field-grid">
|
|
526
|
+
<div class="field">
|
|
527
|
+
<label for="employee_count">Number of Employees</label>
|
|
528
|
+
<input type="number" id="employee_count" min="1" placeholder="e.g. 250" />
|
|
529
|
+
<div class="field-error" id="err-employee_count"></div>
|
|
530
|
+
</div>
|
|
531
|
+
<div class="field">
|
|
532
|
+
<label for="avg_salary">Average Annual Salary ($)</label>
|
|
533
|
+
<input type="number" id="avg_salary" min="0" placeholder="e.g. 65000" />
|
|
534
|
+
<div class="field-error" id="err-avg_salary"></div>
|
|
535
|
+
</div>
|
|
536
|
+
<div class="field">
|
|
537
|
+
<label for="incidents_per_year">Security Incidents per Year</label>
|
|
538
|
+
<input type="number" id="incidents_per_year" min="0" placeholder="e.g. 12" />
|
|
539
|
+
<div class="field-error" id="err-incidents_per_year"></div>
|
|
540
|
+
</div>
|
|
541
|
+
<div class="field">
|
|
542
|
+
<label for="avg_incident_cost">Average Cost per Incident ($)</label>
|
|
543
|
+
<input type="number" id="avg_incident_cost" min="0" placeholder="e.g. 25000" />
|
|
544
|
+
<div class="field-error" id="err-avg_incident_cost"></div>
|
|
545
|
+
</div>
|
|
546
|
+
</div>
|
|
547
|
+
</div>
|
|
548
|
+
|
|
549
|
+
<!-- Panel 1: Training -->
|
|
550
|
+
<div class="tab-panel" data-panel="1">
|
|
551
|
+
<div class="panel-title"><span class="panel-title-icon">🎯</span> Training Parameters</div>
|
|
552
|
+
<div class="field-grid">
|
|
553
|
+
<div class="field">
|
|
554
|
+
<label for="click_rate_before">Phishing Click Rate Before Training (%)</label>
|
|
555
|
+
<input type="number" id="click_rate_before" min="0" max="100" placeholder="e.g. 30" />
|
|
556
|
+
<div class="field-error" id="err-click_rate_before"></div>
|
|
557
|
+
</div>
|
|
558
|
+
<div class="field">
|
|
559
|
+
<label for="expected_reduction">Expected Click Rate Reduction (%)</label>
|
|
560
|
+
<input type="number" id="expected_reduction" min="0" max="100" placeholder="e.g. 70" />
|
|
561
|
+
<div class="field-error" id="err-expected_reduction"></div>
|
|
562
|
+
</div>
|
|
563
|
+
<div class="field full">
|
|
564
|
+
<label for="training_hours">Training Hours per Employee per Year</label>
|
|
565
|
+
<input type="number" id="training_hours" min="0" placeholder="e.g. 4" />
|
|
566
|
+
<div class="field-error" id="err-training_hours"></div>
|
|
567
|
+
</div>
|
|
568
|
+
</div>
|
|
569
|
+
</div>
|
|
570
|
+
|
|
571
|
+
<!-- Panel 2: Costs -->
|
|
572
|
+
<div class="tab-panel" data-panel="2">
|
|
573
|
+
<div class="panel-title"><span class="panel-title-icon">💰</span> Training Costs</div>
|
|
574
|
+
<div class="field-grid">
|
|
575
|
+
<div class="field full">
|
|
576
|
+
<label for="platform_licensing">
|
|
577
|
+
Platform Licensing ($/year total)
|
|
578
|
+
<span class="auto-badge">auto-calc</span>
|
|
579
|
+
</label>
|
|
580
|
+
<input type="number" id="platform_licensing" min="0" placeholder="Auto: $15/user/yr" />
|
|
581
|
+
<div class="field-error" id="err-platform_licensing"></div>
|
|
582
|
+
</div>
|
|
583
|
+
<div class="field">
|
|
584
|
+
<label for="implementation_hours">Implementation Hours</label>
|
|
585
|
+
<input type="number" id="implementation_hours" min="0" placeholder="e.g. 40" />
|
|
586
|
+
<div class="field-error" id="err-implementation_hours"></div>
|
|
587
|
+
</div>
|
|
588
|
+
<div class="field">
|
|
589
|
+
<label for="hourly_rate">Implementation Hourly Rate ($)</label>
|
|
590
|
+
<input type="number" id="hourly_rate" min="0" placeholder="e.g. 75" />
|
|
591
|
+
<div class="field-error" id="err-hourly_rate"></div>
|
|
592
|
+
</div>
|
|
593
|
+
</div>
|
|
594
|
+
</div>
|
|
595
|
+
|
|
596
|
+
<!-- Panel 3: Results -->
|
|
597
|
+
<div class="tab-panel" data-panel="3">
|
|
598
|
+
<div id="results-content">
|
|
599
|
+
<div class="empty-results">
|
|
600
|
+
<div class="icon">📈</div>
|
|
601
|
+
<p>Complete the previous steps and click <strong>Calculate ROI</strong> to see your results.</p>
|
|
602
|
+
</div>
|
|
603
|
+
</div>
|
|
604
|
+
</div>
|
|
605
|
+
|
|
606
|
+
<!-- Navigation -->
|
|
607
|
+
<div class="nav-row">
|
|
608
|
+
<button class="btn btn-secondary" id="btn-prev" disabled>← Previous</button>
|
|
609
|
+
<span class="progress-text" id="progress-text">Step 1 of 4</span>
|
|
610
|
+
<button class="btn btn-primary" id="btn-next">Next →</button>
|
|
611
|
+
</div>
|
|
612
|
+
</div>
|
|
613
|
+
`;
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
// ─── Web Component ────────────────────────────────────────────────────────────
|
|
617
|
+
|
|
618
|
+
class HailbytesROICalculator extends HTMLElement {
|
|
619
|
+
static get observedAttributes() { return ['theme', 'branding']; }
|
|
620
|
+
|
|
621
|
+
constructor() {
|
|
622
|
+
super();
|
|
623
|
+
this._shadow = this.attachShadow({ mode: 'open' });
|
|
624
|
+
this._currentTab = 0;
|
|
625
|
+
this._totalTabs = 4;
|
|
626
|
+
this._results = null;
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
connectedCallback() {
|
|
630
|
+
this._shadow.innerHTML = buildTemplate();
|
|
631
|
+
this._bindEvents();
|
|
632
|
+
this._updateNav();
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
attributeChangedCallback() {
|
|
636
|
+
if (this._shadow.innerHTML) {
|
|
637
|
+
// Re-render preserving form values
|
|
638
|
+
const saved = this._collectAllValues();
|
|
639
|
+
this._shadow.innerHTML = buildTemplate();
|
|
640
|
+
this._restoreValues(saved);
|
|
641
|
+
this._bindEvents();
|
|
642
|
+
this._updateNav();
|
|
643
|
+
if (this._results) this._renderResults(this._results);
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
// ── Public static API ────────────────────────────────────────────────────
|
|
648
|
+
static calculate(inputs) {
|
|
649
|
+
return calculateROI(inputs);
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
// ── Event binding ───────────────────────────────────────────────────────
|
|
653
|
+
_bindEvents() {
|
|
654
|
+
const root = this._shadow;
|
|
655
|
+
|
|
656
|
+
// Tab buttons
|
|
657
|
+
root.querySelectorAll('.tab-btn').forEach(btn => {
|
|
658
|
+
btn.addEventListener('click', () => {
|
|
659
|
+
const tab = parseInt(btn.dataset.tab);
|
|
660
|
+
// Only allow navigating to completed tabs or current+1
|
|
661
|
+
if (tab <= this._currentTab + 1) this._goToTab(tab);
|
|
662
|
+
});
|
|
663
|
+
});
|
|
664
|
+
|
|
665
|
+
// Prev / Next
|
|
666
|
+
root.getElementById('btn-prev').addEventListener('click', () => {
|
|
667
|
+
if (this._currentTab > 0) this._goToTab(this._currentTab - 1);
|
|
668
|
+
});
|
|
669
|
+
root.getElementById('btn-next').addEventListener('click', () => {
|
|
670
|
+
if (this._currentTab === this._totalTabs - 1) {
|
|
671
|
+
this._doCalculate();
|
|
672
|
+
} else {
|
|
673
|
+
if (this._validateCurrentTab()) {
|
|
674
|
+
this._goToTab(this._currentTab + 1);
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
});
|
|
678
|
+
|
|
679
|
+
// Auto-calc platform licensing when employee count changes
|
|
680
|
+
root.getElementById('employee_count').addEventListener('input', () => {
|
|
681
|
+
const empInput = root.getElementById('employee_count');
|
|
682
|
+
const licInput = root.getElementById('platform_licensing');
|
|
683
|
+
const n = parseFloat(empInput.value);
|
|
684
|
+
if (n > 0 && !licInput._manuallyEdited) {
|
|
685
|
+
licInput.value = Math.round(n * 15);
|
|
686
|
+
}
|
|
687
|
+
});
|
|
688
|
+
root.getElementById('platform_licensing').addEventListener('input', () => {
|
|
689
|
+
root.getElementById('platform_licensing')._manuallyEdited = true;
|
|
690
|
+
});
|
|
691
|
+
|
|
692
|
+
// Live validation on blur
|
|
693
|
+
root.querySelectorAll('input').forEach(input => {
|
|
694
|
+
input.addEventListener('blur', () => this._validateField(input));
|
|
695
|
+
input.addEventListener('input', () => {
|
|
696
|
+
if (input.classList.contains('invalid')) this._validateField(input);
|
|
697
|
+
});
|
|
698
|
+
});
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
// ── Navigation ──────────────────────────────────────────────────────────────
|
|
702
|
+
_goToTab(index) {
|
|
703
|
+
const root = this._shadow;
|
|
704
|
+
this._currentTab = index;
|
|
705
|
+
|
|
706
|
+
root.querySelectorAll('.tab-btn').forEach((btn, i) => {
|
|
707
|
+
btn.classList.toggle('active', i === index);
|
|
708
|
+
if (i < index) btn.classList.add('done');
|
|
709
|
+
});
|
|
710
|
+
root.querySelectorAll('.tab-panel').forEach((panel, i) => {
|
|
711
|
+
panel.classList.toggle('active', i === index);
|
|
712
|
+
});
|
|
713
|
+
|
|
714
|
+
this._updateNav();
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
_updateNav() {
|
|
718
|
+
const root = this._shadow;
|
|
719
|
+
const prevBtn = root.getElementById('btn-prev');
|
|
720
|
+
const nextBtn = root.getElementById('btn-next');
|
|
721
|
+
const progressText = root.getElementById('progress-text');
|
|
722
|
+
|
|
723
|
+
prevBtn.disabled = this._currentTab === 0;
|
|
724
|
+
progressText.textContent = `Step ${this._currentTab + 1} of ${this._totalTabs}`;
|
|
725
|
+
|
|
726
|
+
if (this._currentTab === this._totalTabs - 1) {
|
|
727
|
+
nextBtn.textContent = '📊 Calculate ROI';
|
|
728
|
+
} else {
|
|
729
|
+
nextBtn.textContent = 'Next →';
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
// ── Validation ────────────────────────────────────────────────────────────
|
|
734
|
+
_getTabFields(tabIndex) {
|
|
735
|
+
const fieldMap = {
|
|
736
|
+
0: ['employee_count', 'avg_salary', 'incidents_per_year', 'avg_incident_cost'],
|
|
737
|
+
1: ['click_rate_before', 'expected_reduction', 'training_hours'],
|
|
738
|
+
2: ['platform_licensing', 'implementation_hours', 'hourly_rate'],
|
|
739
|
+
3: [],
|
|
740
|
+
};
|
|
741
|
+
return fieldMap[tabIndex] || [];
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
_validateField(inputEl) {
|
|
745
|
+
const root = this._shadow;
|
|
746
|
+
const id = inputEl.id;
|
|
747
|
+
const val = parseFloat(inputEl.value);
|
|
748
|
+
const errEl = root.getElementById(`err-${id}`);
|
|
749
|
+
if (!errEl) return true;
|
|
750
|
+
|
|
751
|
+
let msg = '';
|
|
752
|
+
|
|
753
|
+
const required = ['employee_count', 'avg_salary', 'incidents_per_year', 'avg_incident_cost',
|
|
754
|
+
'click_rate_before', 'expected_reduction', 'training_hours',
|
|
755
|
+
'implementation_hours', 'hourly_rate'];
|
|
756
|
+
|
|
757
|
+
if (required.includes(id)) {
|
|
758
|
+
if (inputEl.value === '' || inputEl.value === null) {
|
|
759
|
+
msg = 'This field is required.';
|
|
760
|
+
} else if (isNaN(val) || val < 0) {
|
|
761
|
+
msg = 'Please enter a valid positive number.';
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
if (id === 'employee_count' && val < 1 && inputEl.value !== '') {
|
|
766
|
+
msg = 'Must be at least 1 employee.';
|
|
767
|
+
}
|
|
768
|
+
if ((id === 'click_rate_before' || id === 'expected_reduction') && val > 100) {
|
|
769
|
+
msg = 'Percentage cannot exceed 100.';
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
// Platform licensing is optional (auto-calc)
|
|
773
|
+
if (id === 'platform_licensing' && inputEl.value !== '' && (isNaN(val) || val < 0)) {
|
|
774
|
+
msg = 'Please enter a valid dollar amount.';
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
errEl.textContent = msg;
|
|
778
|
+
inputEl.classList.toggle('invalid', msg !== '');
|
|
779
|
+
return msg === '';
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
_validateCurrentTab() {
|
|
783
|
+
const fields = this._getTabFields(this._currentTab);
|
|
784
|
+
const root = this._shadow;
|
|
785
|
+
let valid = true;
|
|
786
|
+
fields.forEach(id => {
|
|
787
|
+
const el = root.getElementById(id);
|
|
788
|
+
if (el && !this._validateField(el)) valid = false;
|
|
789
|
+
});
|
|
790
|
+
return valid;
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
// ── Calculation ────────────────────────────────────────────────────────────
|
|
794
|
+
_collectInputs() {
|
|
795
|
+
const g = (id) => parseFloat(this._shadow.getElementById(id)?.value) || 0;
|
|
796
|
+
const empCount = g('employee_count');
|
|
797
|
+
let platformLicensing = parseFloat(this._shadow.getElementById('platform_licensing')?.value);
|
|
798
|
+
if (isNaN(platformLicensing) || platformLicensing === 0) {
|
|
799
|
+
platformLicensing = empCount * 15;
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
return {
|
|
803
|
+
employeeCount: empCount,
|
|
804
|
+
avgSalary: g('avg_salary'),
|
|
805
|
+
incidentsPerYear: g('incidents_per_year'),
|
|
806
|
+
avgIncidentCost: g('avg_incident_cost'),
|
|
807
|
+
clickRateBefore: g('click_rate_before'),
|
|
808
|
+
expectedReduction: g('expected_reduction'),
|
|
809
|
+
trainingHoursPerYear: g('training_hours'),
|
|
810
|
+
platformLicensing,
|
|
811
|
+
implementationHours: g('implementation_hours'),
|
|
812
|
+
hourlyRate: g('hourly_rate'),
|
|
813
|
+
};
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
_collectAllValues() {
|
|
817
|
+
const ids = ['employee_count', 'avg_salary', 'incidents_per_year', 'avg_incident_cost',
|
|
818
|
+
'click_rate_before', 'expected_reduction', 'training_hours',
|
|
819
|
+
'platform_licensing', 'implementation_hours', 'hourly_rate'];
|
|
820
|
+
const vals = {};
|
|
821
|
+
ids.forEach(id => {
|
|
822
|
+
const el = this._shadow.getElementById(id);
|
|
823
|
+
if (el) vals[id] = el.value;
|
|
824
|
+
});
|
|
825
|
+
return vals;
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
_restoreValues(vals) {
|
|
829
|
+
Object.entries(vals).forEach(([id, val]) => {
|
|
830
|
+
const el = this._shadow.getElementById(id);
|
|
831
|
+
if (el) el.value = val;
|
|
832
|
+
});
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
_doCalculate() {
|
|
836
|
+
// Validate all tabs first
|
|
837
|
+
let allValid = true;
|
|
838
|
+
for (let t = 0; t < 3; t++) {
|
|
839
|
+
const fields = this._getTabFields(t);
|
|
840
|
+
const root = this._shadow;
|
|
841
|
+
fields.forEach(id => {
|
|
842
|
+
const el = root.getElementById(id);
|
|
843
|
+
if (el && !this._validateField(el)) { allValid = false; }
|
|
844
|
+
});
|
|
845
|
+
}
|
|
846
|
+
if (!allValid) {
|
|
847
|
+
// Go back to first invalid tab
|
|
848
|
+
for (let t = 0; t < 3; t++) {
|
|
849
|
+
const fields = this._getTabFields(t);
|
|
850
|
+
const hasErr = fields.some(id => {
|
|
851
|
+
const el = this._shadow.getElementById(id);
|
|
852
|
+
return el && el.classList.contains('invalid');
|
|
853
|
+
});
|
|
854
|
+
if (hasErr) { this._goToTab(t); break; }
|
|
855
|
+
}
|
|
856
|
+
return;
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
const inputs = this._collectInputs();
|
|
860
|
+
const results = calculateROI(inputs);
|
|
861
|
+
this._results = results;
|
|
862
|
+
|
|
863
|
+
// Navigate to results tab
|
|
864
|
+
this._goToTab(3);
|
|
865
|
+
this._renderResults(results);
|
|
866
|
+
|
|
867
|
+
// Emit event
|
|
868
|
+
this.dispatchEvent(new CustomEvent('roi-calculated', {
|
|
869
|
+
bubbles: true, composed: true,
|
|
870
|
+
detail: results,
|
|
871
|
+
}));
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
// ── Results rendering ──────────────────────────────────────────────────────
|
|
875
|
+
_renderResults(r) {
|
|
876
|
+
const container = this._shadow.getElementById('results-content');
|
|
877
|
+
if (!container) return;
|
|
878
|
+
|
|
879
|
+
const roiClass = r.roi >= 0 ? 'green' : 'red';
|
|
880
|
+
const netClass = r.netBenefit >= 0 ? 'green' : 'red';
|
|
881
|
+
const roiCalloutClass = r.roi >= 0 ? '' : 'neg';
|
|
882
|
+
|
|
883
|
+
// Bar chart — scale all bars relative to the largest value
|
|
884
|
+
const chartMax = Math.max(r.annualRiskReduction, r.totalTrainingCost, Math.abs(r.netBenefit), 1);
|
|
885
|
+
const pct = (v) => Math.max(2, (Math.abs(v) / chartMax) * 100).toFixed(1);
|
|
886
|
+
|
|
887
|
+
container.innerHTML = `
|
|
888
|
+
<div class="roi-callout">
|
|
889
|
+
<div class="roi-callout-icon">${r.roi >= 0 ? '🚀' : '⚠️'}</div>
|
|
890
|
+
<div class="roi-callout-text">
|
|
891
|
+
<div class="big ${roiCalloutClass}">${fmtPct(r.roi)} ROI</div>
|
|
892
|
+
<div class="sub">${r.roi >= 0
|
|
893
|
+
? `For every $1 invested, you gain $${(r.roi / 100).toFixed(2)} back`
|
|
894
|
+
: 'Training costs exceed projected risk reduction'}</div>
|
|
895
|
+
</div>
|
|
896
|
+
</div>
|
|
897
|
+
|
|
898
|
+
<div class="metrics-grid">
|
|
899
|
+
<div class="metric-card highlight">
|
|
900
|
+
<div class="metric-icon">🛡️</div>
|
|
901
|
+
<div class="metric-label">Risk Reduction</div>
|
|
902
|
+
<div class="metric-value accent">${fmt$(r.annualRiskReduction)}</div>
|
|
903
|
+
</div>
|
|
904
|
+
<div class="metric-card">
|
|
905
|
+
<div class="metric-icon">💸</div>
|
|
906
|
+
<div class="metric-label">Training Cost</div>
|
|
907
|
+
<div class="metric-value">${fmt$(r.totalTrainingCost)}</div>
|
|
908
|
+
</div>
|
|
909
|
+
<div class="metric-card ${r.netBenefit >= 0 ? 'positive' : 'negative'}">
|
|
910
|
+
<div class="metric-icon">${r.netBenefit >= 0 ? '✅' : '❌'}</div>
|
|
911
|
+
<div class="metric-label">Net Benefit</div>
|
|
912
|
+
<div class="metric-value ${netClass}">${fmt$(r.netBenefit)}</div>
|
|
913
|
+
</div>
|
|
914
|
+
<div class="metric-card">
|
|
915
|
+
<div class="metric-icon">⏱️</div>
|
|
916
|
+
<div class="metric-label">Payback Period</div>
|
|
917
|
+
<div class="metric-value">${isFinite(r.paybackMonths) ? fmtNum(r.paybackMonths, 1) + ' mo' : '—'}</div>
|
|
918
|
+
</div>
|
|
919
|
+
<div class="metric-card">
|
|
920
|
+
<div class="metric-icon">🎯</div>
|
|
921
|
+
<div class="metric-label">Click Rate After</div>
|
|
922
|
+
<div class="metric-value">${fmtNum(r.clickRateAfter, 1)}%</div>
|
|
923
|
+
</div>
|
|
924
|
+
<div class="metric-card">
|
|
925
|
+
<div class="metric-icon">🚫</div>
|
|
926
|
+
<div class="metric-label">Incidents Prevented</div>
|
|
927
|
+
<div class="metric-value">${fmtNum(r.preventedIncidents, 1)}/yr</div>
|
|
928
|
+
</div>
|
|
929
|
+
</div>
|
|
930
|
+
|
|
931
|
+
<div class="chart-section">
|
|
932
|
+
<div class="chart-title">📊 Financial Comparison</div>
|
|
933
|
+
<div class="bar-chart">
|
|
934
|
+
<div class="bar-row">
|
|
935
|
+
<div class="bar-label">
|
|
936
|
+
<span>Annual Risk Reduction</span>
|
|
937
|
+
<strong>${fmt$(r.annualRiskReduction)}</strong>
|
|
938
|
+
</div>
|
|
939
|
+
<div class="bar-track">
|
|
940
|
+
<div class="bar-fill risk-reduction" style="width:${pct(r.annualRiskReduction)}%"></div>
|
|
941
|
+
</div>
|
|
942
|
+
</div>
|
|
943
|
+
<div class="bar-row">
|
|
944
|
+
<div class="bar-label">
|
|
945
|
+
<span>Total Training Cost</span>
|
|
946
|
+
<strong>${fmt$(r.totalTrainingCost)}</strong>
|
|
947
|
+
</div>
|
|
948
|
+
<div class="bar-track">
|
|
949
|
+
<div class="bar-fill training-cost" style="width:${pct(r.totalTrainingCost)}%"></div>
|
|
950
|
+
</div>
|
|
951
|
+
</div>
|
|
952
|
+
<div class="bar-row">
|
|
953
|
+
<div class="bar-label">
|
|
954
|
+
<span>Net Benefit</span>
|
|
955
|
+
<strong>${fmt$(r.netBenefit)}</strong>
|
|
956
|
+
</div>
|
|
957
|
+
<div class="bar-track">
|
|
958
|
+
<div class="bar-fill net-benefit" style="width:${pct(r.netBenefit)}%"></div>
|
|
959
|
+
</div>
|
|
960
|
+
</div>
|
|
961
|
+
</div>
|
|
962
|
+
</div>
|
|
963
|
+
|
|
964
|
+
<div class="breakdown-section">
|
|
965
|
+
<div class="breakdown-title">Cost Breakdown</div>
|
|
966
|
+
<div class="breakdown-rows" style="background:var(--bg);border:1px solid var(--border);border-radius:8px;overflow:hidden;">
|
|
967
|
+
<div class="breakdown-row">
|
|
968
|
+
<span class="breakdown-key">Platform Licensing</span>
|
|
969
|
+
<span class="breakdown-val">${fmt$(r.platformLicensing)}</span>
|
|
970
|
+
</div>
|
|
971
|
+
<div class="breakdown-row">
|
|
972
|
+
<span class="breakdown-key">Implementation Cost</span>
|
|
973
|
+
<span class="breakdown-val">${fmt$(r.implementationCost)}</span>
|
|
974
|
+
</div>
|
|
975
|
+
<div class="breakdown-row">
|
|
976
|
+
<span class="breakdown-key">Employee Productivity Cost</span>
|
|
977
|
+
<span class="breakdown-val">${fmt$(r.productivityCost)}</span>
|
|
978
|
+
</div>
|
|
979
|
+
<div class="breakdown-row total">
|
|
980
|
+
<span class="breakdown-key">Total Investment</span>
|
|
981
|
+
<span class="breakdown-val">${fmt$(r.totalTrainingCost)}</span>
|
|
982
|
+
</div>
|
|
983
|
+
</div>
|
|
984
|
+
</div>
|
|
985
|
+
|
|
986
|
+
<div class="results-cta hb-branding">
|
|
987
|
+
<p>Want to see a detailed proposal for your organization?</p>
|
|
988
|
+
<a class="cta-link" href="https://hailbytes.com" target="_blank" rel="noopener">
|
|
989
|
+
🚀 Talk to a HailBytes Expert
|
|
990
|
+
</a>
|
|
991
|
+
</div>
|
|
992
|
+
`;
|
|
993
|
+
}
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
customElements.define('hailbytes-roi-calculator', HailbytesROICalculator);
|
|
997
|
+
|
|
998
|
+
export default HailbytesROICalculator;
|
|
999
|
+
export { calculateROI };
|