1// Shared per-module discount display for the calculator and its price modal. 2window.HRONUpdateComboBadges = function (container, selected, sizes, serverRates, enterprise) { 3 if (!container) return; 4 var config = window.HRONNewPrice || {}; 5 var limits = config.enterpriseLimits || {}; 6 var count = Object.keys(selected).filter(function (key) { return selected[key]; }).length; 7 var unavailable = enterprise || sizes.employees > limits.employees || sizes.jobslots > limits.jobslots; 8 var lang = (document.documentElement.lang || 'da').slice(0, 2).toLowerCase(); 9 var labels = { en: ['with Staff', 'combo'], de: ['mit Staff', 'Kombi'], da: ['med Staff', 'kombination'], nb: ['med Staff', 'kombinasjon'] }; 10 var words = labels[lang] || labels.en; 11 container.querySelectorAll('.module-item[data-module], .hron-newprice-system-card[data-module]').forEach(function (card) { 12 var key = card.dataset.module; 13 var heading = card.querySelector('.module-title, h4'); 14 if (!heading) return; 15 var badge = heading.querySelector('.hron-combo-badge'); 16 if (!badge) { 17 badge = document.createElement('span'); 18 badge.className = 'hron-combo-badge'; 19 badge.hidden = true; 20 badge.setAttribute('aria-live', 'polite'); 21 heading.appendChild(badge); 22 } 23 var rates = (config.comboRates || {})[key] || {}; 24 var rate = serverRates && typeof serverRates[key] === 'number' 25 ? serverRates[key] * 100 26 : Number(key === 'staff' || !selected.staff ? rates.withoutStaff : rates.withStaff); 27 var visible = !unavailable && count > 1 && selected[key] && Number.isFinite(rate) && rate > 0; 28 if (!visible) { 29 badge.hidden = true; 30 badge.textContent = ''; 31 return; 32 } 33 var text = 'â' + Math.round(rate) + '% · ' + (selected.staff && key !== 'staff' ? words[0] : words[1]); 34 var changed = badge.hidden || badge.textContent !== text; 35 badge.textContent = text; 36 badge.hidden = false; 37 if (changed && badge.animate && !window.matchMedia('(prefers-reduced-motion: reduce)').matches) { 38 badge.getAnimations().forEach(function (animation) { animation.cancel(); }); 39 badge.animate([{ opacity: 0.35, transform: 'translateY(3px)' }, { opacity: 1, transform: 'translateY(0)' }], { duration: 180, easing: 'ease-out' }); 40 } 41 }); 42}; 43 44// Author: Eser Sariyar <[email protected]> | Company: HR-ON (hr-on.com) 45// Define the updateIconsVisibility function globally to be used throughout the app 46window.updateIconsVisibility = function () { 47 var root = document.querySelector('.hron-new-price-root'); 48 if (!root) return; 49 50 var miniModuleContainer = root.querySelector('.mini-module-icons'); 51 if (!miniModuleContainer) return; 52 53 // Get all selected modules 54 var selectedModules = []; 55 var moduleItems = root.querySelectorAll('.module-item.selected'); 56 moduleItems.forEach(function (module) { 57 var moduleId = module.getAttribute('data-module'); 58 if (moduleId) { 59 selectedModules.push(moduleId); 60 } 61 }); 62 63 // Update visibility of mini module icons 64 var miniIcons = miniModuleContainer.querySelectorAll('.mini-module-icon'); 65 miniIcons.forEach(function (icon) { 66 var moduleId = icon.getAttribute('data-module'); 67 if (moduleId) { 68 // Show only if the module is selected 69 var isSelected = selectedModules.includes(moduleId); 70 icon.style.display = isSelected ? '' : 'none'; 71 } 72 }); 73 74 // Reorder visible icons to remove gaps 75 var visibleIcons = Array.from(miniIcons).filter(function (icon) { 76 return icon.style.display !== 'none'; 77 }); 78 79 visibleIcons.forEach(function (icon, index) { 80 var leftOffset = index > 0 ? -3 * index : 0; 81 icon.style.transition = 'all 0.3s ease'; 82 icon.style.marginLeft = leftOffset + 'px'; 83 icon.style.marginRight = (index === visibleIcons.length - 2) ? '2px' : '-2px'; 84 icon.style.zIndex = (index + 1).toString(); 85 }); 86}; 87 88(function () { 89 function qs(root, sel) { 90 return root.querySelector(sel); 91 } 92 93 function qsa(root, sel) { 94 return Array.prototype.slice.call(root.querySelectorAll(sel)); 95 } 96 97 var hronWebhookFired = false;
98 99 function getUtmParams() { 100 var result = {}; 101 var keys = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'utm_id']; 102 var savedAtKey = 'hron_utm_saved_at'; 103 var ttlMs = 90 * 24 * 60 * 60 * 1000; 104 try { 105 var search = window.location.search || ''; 106 var params = new URLSearchParams(search); 107 keys.forEach(function (key) { 108 var val = params.get(key); 109 if (val) result[key] = val; 110 }); 111 112 // The current URL is authoritative when it carries campaign data. 113 if (Object.keys(result).length) return result; 114 115 // Price submission is AJAX-based and does not serialize the hidden 116 // fields added to regular forms, so use recent stored attribution. 117 var savedAt = Number(window.localStorage.getItem(savedAtKey)); 118 if (!savedAt || Date.now() - savedAt > ttlMs) { 119 keys.forEach(function (key) { 120 window.localStorage.removeItem(key); 121 }); 122 window.localStorage.removeItem(savedAtKey); 123 return result; 124 } 125 126 keys.forEach(function (key) { 127 var val = window.localStorage.getItem(key); 128 if (val) result[key] = val; 129 }); 130 } catch (_) { } 131 return result; 132 } 133 134 /** 135 * Refresh the WP nonce via a lightweight AJAX call. 136 * Returns a Promise that resolves once HRONNewPrice.nonce is updated. 137 */ 138 var _nonceRefreshing = null; 139 function refreshHRONNonce() { 140 if (_nonceRefreshing) return _nonceRefreshing; 141 if (!window.HRONNewPrice) return Promise.resolve(); 142 var ajaxUrl = HRONNewPrice.ajaxUrl || '/wp-admin/admin-ajax.php'; 143 var action = HRONNewPrice.refreshNonceAction || 'hron_refresh_nonce'; 144 var fd = new FormData(); 145 fd.append('action', action); 146 _nonceRefreshing = fetch(ajaxUrl, { method: 'POST', body: fd }) 147 .then(function (r) { return r.json(); }) 148 .then(function (resp) { 149 if (resp && resp.success && resp.data && resp.data.nonce) { 150 HRONNewPrice.nonce = resp.data.nonce; 151 HRONNewPrice.nonceTime = resp.data.nonceTime || Math.floor(Date.now() / 1000); 152 } 153 }) 154 .catch(function () { /* silent */ }) 155 .then(function () { _nonceRefreshing = null; }); 156 return _nonceRefreshing; 157 } 158 159 /** 160 * Proactively refresh nonce if the page has been open > 10 hours. 161 */ 162 function ensureFreshNonce() { 163 if (!window.HRONNewPrice || !HRONNewPrice.nonceTime) return; 164 var ageSeconds = Math.floor(Date.now() / 1000) - parseInt(HRONNewPrice.nonceTime, 10); 165 if (ageSeconds > 10 * 3600) { 166 refreshHRONNonce(); 167 } 168 } 169 170 document.addEventListener('DOMContentLoaded', function () { 171 // Proactively refresh nonce if it may be expired (cached page) 172 ensureFreshNonce(); 173 174 (function initBottomPage() { 175 var pricePage = document.querySelector('.hron-price-bottom-page'); 176 if (!pricePage) return; 177 var tabs = document.querySelectorAll('.hron-price-bottom-tab'); 178 var tabsNav = document.querySelector('.hron-price-bottom-tabs'); 179 var tabsContainer = document.querySelector('.hron-price-bottom-tabs-container'); 180 var sections = document.querySelectorAll('.hron-price-bottom-card'); 181 var pluginsColumn = document.querySelector('.hron-price-bottom-plugins-column'); 182 if (!tabsNav || !tabsContainer || !sections.length) return; 183 184 var pageTop = 0; 185 var tabsContainerTop = 0; 186 var footerTop = 0; 187 var lastSection = sections[sections.length - 1]; 188 var tabScrollLock = false; 189 var lockedTabIndex = -1; 190 var lockedTargetY = null; 191 var tabLockTimer = null; 192 193 var pluginsColumnOriginalTop = 0; 194 var pluginsColumnOriginalPosition = ''; 195 196 function recalcMetrics() { 197 var pageRect = pricePage.getBoundingClientRect(); 198 pageTop = pageRect.top + window.scrollY; 199 var tabsContainerRect = tabsContainer.getBoundingClientRect(); 200 tabsContainerTop = tabsContainerRect.top + window.scrollY; 201 footerTop = lastSection.offsetTop + lastSection.offsetHeight + pageTop; 202 203 if (pluginsColumn) { 204 var style = window.getComputedStyle(pluginsColumn); 205 pluginsColumnOriginalPosition = style.position; 206 pluginsColumnOriginalTop = pluginsColumn.getBoundingClientRect().top + window.scrollY; 207 } 208 } 209 recalcMetrics(); 210 211 function setActiveTab(index) { 212 Array.prototype.forEach.call(tabs, function (t) { 213 t.classList.remove('active'); 214 }); 215 if (tabs[index]) { 216 tabs[index].classList.add('active'); 217 } 218 } 219 220 Array.prototype.forEach.call(tabs, function (tab, index) { 221 tab.addEventListener('click', function () {
222 setActiveTab(index); 223 var viewportHeight = window.innerHeight; 224 var sectionHeight = sections[index].offsetHeight; 225 var headerOffset = 120; 226 var centeredOffset; 227 if (sectionHeight < viewportHeight - headerOffset) { 228 centeredOffset = headerOffset + ((viewportHeight - headerOffset - sectionHeight) / 2); 229 } else { 230 centeredOffset = headerOffset + 50; 231 } 232 var targetY = sections[index].offsetTop + pageTop - centeredOffset; 233 lockedTabIndex = index; 234 lockedTargetY = targetY; 235 tabScrollLock = true; 236 if (tabLockTimer) { 237 clearTimeout(tabLockTimer); 238 } 239 tabLockTimer = setTimeout(function () { 240 tabScrollLock = false; 241 lockedTabIndex = -1; 242 lockedTargetY = null; 243 updateStickyState(); 244 }, 1000); 245 window.scrollTo({ 246 top: targetY, 247 behavior: 'smooth' 248 }); 249 }); 250 }); 251 252 function updateStickyState() { 253 var currentScroll = window.scrollY; 254 var windowHeight = window.innerHeight; 255 256 var tabsNavHeight = tabsNav.offsetHeight; 257 var bottomEdge = footerTop - 30; 258 var startSticky = tabsContainerTop - 80; 259 var endSticky = bottomEdge - tabsNavHeight - 80; 260 261 var backToTopButton = document.getElementById('hron-back-to-top'); 262 if (backToTopButton) { 263 var buttonShowPosition = startSticky + (endSticky - startSticky) * 0.6; // %60 264 265 if (currentScroll >= buttonShowPosition) { 266 backToTopButton.classList.add('visible'); 267 } else { 268 backToTopButton.classList.remove('visible'); 269 } 270 } 271 272 if (currentScroll > startSticky && currentScroll < endSticky) { 273 tabsNav.classList.add('sticky'); 274 tabsNav.classList.remove('absolute'); 275 } else if (currentScroll >= endSticky) { 276 tabsNav.classList.remove('sticky'); 277 tabsNav.classList.add('absolute'); 278 } else { 279 tabsNav.classList.remove('sticky'); 280 tabsNav.classList.remove('absolute'); 281 } 282 var canAutoSyncTab = true; 283 if (tabScrollLock && lockedTabIndex > -1) {
284 setActiveTab(lockedTabIndex); 285 if (lockedTargetY !== null && Math.abs(window.scrollY - lockedTargetY) <= 6) { 286 tabScrollLock = false; 287 lockedTabIndex = -1; 288 lockedTargetY = null; 289 if (tabLockTimer) { 290 clearTimeout(tabLockTimer); 291 tabLockTimer = null; 292 } 293 } else { 294 canAutoSyncTab = false; 295 } 296 } 297 298 if (canAutoSyncTab) { 299 var activeIndex = 0; 300 // Use a viewport-relative focus line for scrollspy. 301 // A dynamic anchor works better for mixed section heights 302 // and prevents snapping back to the previous tab. 303 var anchorY = currentScroll + Math.max(220, Math.round(windowHeight * 0.45)); 304 Array.prototype.forEach.call(sections, function (section, index) { 305 var sectionTop = pageTop + section.offsetTop; 306 if (anchorY >= sectionTop) { 307 activeIndex = index; 308 } 309 }); 310 var currentActive = document.querySelector('.hron-price-bottom-tab.active'); 311 if (currentActive && currentActive !== tabs[activeIndex]) { 312 currentActive.classList.remove('active'); 313 tabs[activeIndex].classList.add('active'); 314 } else if (!currentActive) { 315 tabs[activeIndex].classList.add('active'); 316 } 317 } 318 319 if (window.innerWidth <= 880) { 320 if (!tabsNav.classList.contains('sticky')) { 321 tabsNav.style.width = '100%'; 322 tabsNav.style.maxWidth = '300px'; 323 } else { 324 tabsNav.style.width = ''; 325 tabsNav.style.maxWidth = ''; 326 } 327 } else { 328 tabsNav.style.width = ''; 329 tabsNav.style.maxWidth = ''; 330 } 331 } 332 333 (function () { 334 if (!pluginsColumn) return; 335 336 var pluginsInner = pluginsColumn.querySelector('.hron-price-bottom-plugins-inner') || pluginsColumn; 337 338 var colRectInitial = null; 339 340 function readTabsTop() { 341 var cs = window.getComputedStyle(tabsNav); 342 var top = parseInt(cs.top, 10); 343 if (!isFinite(top) || cs.position !== 'fixed') top = 100; 344 return top; 345 } 346 347 function measureCol() { 348 var r = pluginsColumn.getBoundingClientRect(); 349 colRectInitial = { 350 left: r.left + window.scrollX, 351 width: r.width, 352 topPage: r.top + window.scrollY 353 }; 354 } 355 356 function applySidebarState() { 357 358 if (window.innerWidth <= 880) { 359 pluginsColumn.style.height = 'auto'; 360 pluginsInner.style.position = ''; 361 pluginsInner.style.top = ''; 362 pluginsInner.style.left = ''; 363 pluginsInner.style.width = ''; 364 return; 365 } 366 367 if (!colRectInitial) measureCol(); 368 369 var currentScroll = window.scrollY; 370 var tabsNavHeight = tabsNav.offsetHeight; 371 var bottomEdge = footerTop - 30; 372 var startSticky = tabsContainerTop - 80; 373 var panelH = pluginsInner.offsetHeight; 374 var endSticky = bottomEdge - panelH - 80; 375 376 if (currentScroll > startSticky && currentScroll < endSticky) { 377 pluginsColumn.style.height = panelH + 'px';
378 pluginsColumn.style.marginTop = '20px'; 379 pluginsInner.style.position = 'fixed'; 380 pluginsInner.style.top = readTabsTop() + 'px'; 381 pluginsInner.style.marginTop = '80px'; 382 pluginsInner.style.left = colRectInitial.left + 'px'; 383 pluginsInner.style.width = 270 + 'px'; 384 385 } else if (currentScroll >= endSticky) { 386 var desiredTopPage = bottomEdge - panelH; 387 var topRelative = desiredTopPage - colRectInitial.topPage; 388 389 pluginsColumn.style.height = panelH + 'px'; 390 pluginsColumn.style.marginTop = '20px'; 391 pluginsInner.style.position = 'absolute'; 392 pluginsInner.style.top = topRelative + 'px'; 393 pluginsInner.style.marginTop = '80px'; 394 pluginsInner.style.left = '0px'; 395 pluginsInner.style.width = '100%'; 396 397 } else { 398 pluginsColumn.style.height = 'auto'; 399 pluginsInner.style.position = ''; 400 pluginsInner.style.top = '0'; 401 pluginsInner.style.marginTop = '0'; 402 pluginsInner.style.left = ''; 403 pluginsInner.style.width = ''; 404 pluginsColumn.style.top = '0'; 405 pluginsColumn.style.marginTop = '0'; 406 } 407 } 408 409 var _origUpdate = updateStickyState; 410 updateStickyState = function () { 411 _origUpdate(); 412 applySidebarState(); 413 }; 414 415 window.addEventListener('resize', function () { 416 colRectInitial = null; 417 applySidebarState(); 418 }); 419 window.addEventListener('load', function () { 420 colRectInitial = null; 421 applySidebarState(); 422 }); 423 document.addEventListener('DOMContentLoaded', function () { 424 colRectInitial = null; 425 applySidebarState(); 426 }); 427 applySidebarState(); 428 })(); 429 430 tabsNav.classList.remove('sticky'); 431 tabsNav.classList.remove('absolute'); 432 433 window.addEventListener('scroll', function () { 434 updateStickyState(); 435 }); 436 437 updateStickyState(); 438 setTimeout(function () { 439 recalcMetrics(); 440 updateStickyState(); 441 }, 100); 442 443 window.addEventListener('resize', function () { 444 recalcMetrics(); 445 updateStickyState(); 446 }); 447 448 window.addEventListener('load', function () { 449 recalcMetrics(); 450 updateStickyState(); 451 }); 452 })(); 453 454 if (!document.getElementById('tooltip-container')) { 455 var tooltipContainer = document.createElement('div'); 456 tooltipContainer.id = 'tooltip-container'; 457 document.body.appendChild(tooltipContainer); 458 } 459 qsa(document, '.hron-new-price-root').forEach(function (root) { 460 var options = qsa(root, '.hron-price-option'); 461 var contactSection = qs(root, '#contact-section'); 462 var suiteInputs = qs(root, '#suite-inputs'); 463 var customModules = qs(root, '#custom-modules'); 464 var jobSlotsInput = qs(root, '#job-slots-input'); 465 var employeesInput = qs(root, '#employees-input'); 466 var jobSlotsGroup = qs(root, '#job-slots-group'); 467 var employeesGroup = qs(root, '#employees-group'); 468 var isSuiteSelected = false; 469 470 var currencyRate = window.HRONNewPrice ? window.HRONNewPrice.currencyRate : 0.134328; 471 var currentCurrency = 'DKK'; 472 var originalPrices = {}; 473 var selectedModules = ['staff', 'recruit', 'boarding', 'wellbeing', 'comply']; 474 var effectiveDiscountRate = 0; 475 // Until a price call comes back we do not know the saving, so the badge 476 // must stay hidden rather than show the placeholder from the markup. 477 var effectiveDiscountKnown = false;
478 var discountRefreshTimer = null; 479 var priceRequestSeq = 0; 480 // What the pill currently shows, and the animations in flight for it. 481 var displayedDiscountRate = null; 482 var discountAnimFrame = null; 483 var discountPulseTimer = null; 484 // Last standalone price per module, with the sizes they were computed for. 485 // Lets the badge repaint instantly when a module is ticked or unticked. 486 var standaloneCache = null; 487 var pluginUrl = window.HRONNewPrice && window.HRONNewPrice.pluginUrl ? window.HRONNewPrice.pluginUrl : '/wp-content/plugins/HRON-New-Price/'; 488 489 function setupTooltips() { 490 qsa(root, '.module-help-icon').forEach(function (icon) { 491 icon.addEventListener('mouseenter', function () { 492 var tooltip = icon.querySelector('.tooltip'); 493 if (!tooltip) return; 494 tooltip.hidden = false; 495 tooltip.setAttribute('aria-hidden', 'false'); 496 var rect = icon.getBoundingClientRect(); 497 var isInputIcon = !!icon.closest('.help-input-group'); 498 if (isInputIcon) { 499 tooltip.style.left = (rect.left - 10) + 'px'; 500 tooltip.style.top = (rect.top - 15) + 'px'; 501 tooltip.style.transform = 'translate(-50%, -100%)'; 502 } else { 503 tooltip.style.left = (rect.left + rect.width / 2) + 'px'; 504 tooltip.style.top = (rect.top - 15) + 'px'; 505 tooltip.style.transform = 'translate(-50%, -100%)'; 506 } 507 tooltip.classList.add('tooltip-active'); 508 }); 509 icon.addEventListener('mouseleave', function () { 510 var tooltip = icon.querySelector('.tooltip'); 511 if (tooltip) { 512 tooltip.classList.remove('tooltip-active'); 513 tooltip.hidden = true; 514 tooltip.setAttribute('aria-hidden', 'true'); 515 } 516 }); 517 }); 518 } 519 520 function toggleInputs() { 521 if (isSuiteSelected) { 522 if (jobSlotsGroup) jobSlotsGroup.style.display = 'block'; 523 if (employeesGroup) employeesGroup.style.display = 'block'; 524 if (suiteInputs) suiteInputs.classList.remove('hide'); 525 // Keep custom modules visible even in Suite mode 526 if (customModules) customModules.classList.add('show'); 527 } else { 528 if (customModules) customModules.classList.add('show'); 529 if (contactSection) contactSection.classList.remove('show'); 530 531 // Check specific module selections - always get fresh status 532 var recruitSel = !!qs(root, '.module-item[data-module="recruit"].selected'); 533 var staffSel = !!qs(root, '.module-item[data-module="staff"].selected'); 534 var boardingSel = !!qs(root, '.module-item[data-module="boarding"].selected'); 535 var wellbeingSel = !!qs(root, '.module-item[data-module="wellbeing"].selected'); 536 var complySel = !!qs(root, '.module-item[data-module="comply"].selected'); 537 538 // Any module selected at all? 539 var hasSelectedModules = recruitSel || staffSel || boardingSel || wellbeingSel || complySel; 540 541 542 // Show/hide job slots input ONLY if recruit is selected 543 if (jobSlotsGroup) { 544 jobSlotsGroup.style.display = recruitSel ? 'block' : 'none'; 545 } 546 547 // Show/hide employees input if any employee-based module is selected 548 var showEmployees = staffSel || boardingSel || wellbeingSel || complySel; 549 if (employeesGroup) { 550 employeesGroup.style.display = showEmployees ? 'block' : 'none'; 551 552 } 553 554 // Master control of suite-inputs div 555 if (suiteInputs) { 556 if (!hasSelectedModules) { 557 // No modules selected, hide everything 558 suiteInputs.classList.add('hide'); 559 } else { 560 // At least one module selected, show the container 561 suiteInputs.classList.remove('hide'); 562 } 563 } 564 } 565 } 566 567 function checkAndShowContactSection() { 568 if (!contactSection) return; 569 var show = false;
570 571 // Always get the latest module selection state directly from the DOM 572 var recruitSel = !!qs(root, '.module-item[data-module="recruit"].selected'); 573 var staffSel = !!qs(root, '.module-item[data-module="staff"].selected'); 574 var boardingSel = !!qs(root, '.module-item[data-module="boarding"].selected'); 575 var wellbeingSel = !!qs(root, '.module-item[data-module="wellbeing"].selected'); 576 var complySel = !!qs(root, '.module-item[data-module="comply"].selected'); 577 var hasSelectedModules = recruitSel || staffSel || boardingSel || wellbeingSel || complySel; 578 579 if (!hasSelectedModules) { 580 // No modules selected, don't show contact section and hide inputs 581 show = false; 582 if (jobSlotsGroup) jobSlotsGroup.style.display = 'none'; 583 if (employeesGroup) employeesGroup.style.display = 'none'; 584 if (suiteInputs) suiteInputs.classList.add('hide'); 585 } else if (isSuiteSelected) { 586 var hasJobSlots = jobSlotsInput && jobSlotsInput.value.trim() !== ''; 587 var hasEmployees = employeesInput && employeesInput.value.trim() !== ''; 588 show = hasJobSlots && hasEmployees; 589 } else { 590 // We have at least one module selected 591 592 // Check if we need job slots input (only if recruit is selected) 593 var needsJobSlots = recruitSel; 594 if (jobSlotsGroup) { 595 jobSlotsGroup.style.display = needsJobSlots ? 'block' : 'none'; 596 } 597 598 // Comply is priced per employee too, so it belongs here. Leaving it 599 // out hid the employees input for a Comply-only deal, which then 600 // had no size to price against. 601 var needsEmployees = staffSel || boardingSel || wellbeingSel || complySel; 602 if (employeesGroup) { 603 employeesGroup.style.display = needsEmployees ? 'block' : 'none'; 604 } 605 606 // For input values 607 var hasJob = jobSlotsInput && jobSlotsInput.value.trim() !== ''; 608 var hasEmp = employeesInput && employeesInput.value.trim() !== ''; 609 610 // Logic for showing contact section 611 if (needsJobSlots && !needsEmployees) { 612 // Only recruit selected, need job slots value 613 show = hasJob; 614 } 615 else if (!needsJobSlots && needsEmployees) { 616 // Only employee-based modules, need employees value 617 show = hasEmp; 618 } 619 else if (needsJobSlots && needsEmployees) { 620 // Both kinds selected, need both values 621 show = hasJob && hasEmp; 622 } 623 } 624 625 contactSection.classList.toggle('show', show); 626 } 627 628 setupTooltips(); 629 toggleInputs(); 630 631 // Activate all modules by default since we start with custom modules 632 selectedModules = []; // Reset the selectedModules array defined at higher scope 633 qsa(root, '.module-item').forEach(function (module) { 634 module.classList.add('selected'); 635 var moduleId = module.getAttribute('data-module'); 636 if (moduleId) { 637 selectedModules.push(moduleId); 638 } 639 }); 640 toggleInputs(); 641 642 // Add event listener for the custom-chip section 643 var customChip = qs(root, '.custom-chip'); 644 if (customChip) { 645 customChip.addEventListener('click', function (e) { 646 // Always prevent default behavior for custom-chip click 647 e.stopPropagation(); 648 e.preventDefault(); 649 650 // Prevent the click from showing inputs if no modules are selected 651 var hasSelectedModules = selectedModules.length > 0; 652 if (!hasSelectedModules) { 653 return; 654 } 655 656 }); 657 } 658 659 options.forEach(function (option, idx) { 660 option.addEventListener('click', function () { 661 options.forEach(function (o) { 662 o.classList.remove('selected'); 663 }); 664 option.classList.add('selected'); 665 // Only a genuine suite option (i.e. more than one choice) may set 666 // this. There is a single "compose your own" option today, and the 667 // module items sit inside it, so every module click bubbles here - 668 // latching this to true would freeze the selection at all five. 669 isSuiteSelected = (options.length > 1 && idx === 0); 670 toggleInputs(); 671 checkAndShowContactSection(); 672 updateDiscountNow(); 673 scheduleDiscountRefresh(); 674 }); 675 }); 676 677 if (jobSlotsInput) { 678 jobSlotsInput.addEventListener('input', function () { 679 680 var value = this.value.replace(/[^0-9]/g, ''); 681 if (value === '0' || value.startsWith('0')) value = value.replace(/^0+/, ''); 682 this.value = value; 683 jobSlotsInput.classList.remove('error'); 684 jobSlotsInput.removeAttribute('aria-invalid'); 685 var fg = jobSlotsInput.closest('.form-group'); 686 if (fg) fg.classList.remove('has-error'); 687 if (fg) { if (value) fg.classList.add('has-value'); else fg.classList.remove('has-value'); } 688 jobSlotsInput.style.borderColor = ''; 689 jobSlotsInput.style.boxShadow = ''; 690 checkAndShowContactSection(); 691 }); 692 693 jobSlotsInput.addEventListener('paste', function (e) { 694 e.preventDefault(); 695 var pastedText = (e.clipboardData || window.clipboardData).getData('text'); 696 var numericOnly = pastedText.replace(/[^0-9]/g, ''); 697 if (numericOnly) { 698 this.value = numericOnly; 699 var fg = jobSlotsInput.closest('.form-group'); 700 if (fg) fg.classList.add('has-value'); 701 checkAndShowContactSection(); 702 } 703 }); 704 } 705 if (employeesInput) { 706 employeesInput.addEventListener('input', function () { 707 708 var value = this.value.replace(/[^0-9]/g, ''); 709 if (value === '0' || value.startsWith('0')) value = value.replace(/^0+/, ''); 710 this.value = value; 711 employeesInput.classList.remove('error'); 712 employeesInput.removeAttribute('aria-invalid'); 713 var fg = employeesInput.closest('.form-group'); 714 if (fg) fg.classList.remove('has-error'); 715 if (fg) { if (value) fg.classList.add('has-value'); else fg.classList.remove('has-value'); } 716 employeesInput.style.borderColor = ''; 717 employeesInput.style.boxShadow = ''; 718 checkAndShowContactSection(); 719 }); 720 721 employeesInput.addEventListener('paste', function (e) { 722 e.preventDefault(); 723 var pastedText = (e.clipboardData || window.clipboardData).getData('text'); 724 var numericOnly = pastedText.replace(/[^0-9]/g, ''); 725 if (numericOnly) { 726 this.value = numericOnly; 727 var fg = employeesInput.closest('.form-group'); 728 if (fg) fg.classList.add('has-value'); 729 checkAndShowContactSection(); 730 } 731 }); 732 } 733 734 var emailField = qs(root, 'input[type="email"]'); 735 if (emailField) { 736 737 emailField.addEventListener('keydown', function (e) { 738 if (e.key === ' ' || e.code === 'Space' || e.keyCode === 32) { 739 e.preventDefault(); 740 } 741 }); 742 743 var allowedRegex = /^[a-zA-Z0-9.!#$%&'*+\/=?^_`{|}~@-]+$/; 744 745 emailField.addEventListener('beforeinput', function (e) { 746 if (e.inputType === 'insertText') { 747 var data = e.data || ''; 748 if (!allowedRegex.test(data)) { 749 e.preventDefault(); 750 } 751 } 752 }); 753 754 emailField.addEventListener('paste', function (e) { 755 var pastedText = (e.clipboardData || window.clipboardData).getData('text'); 756 var cleaned = (pastedText || '').replace(/[^a-zA-Z0-9.!#$%&'*+\/=?^_`{|}~@-]/g, ''); 757 if (cleaned !== pastedText) { 758 e.preventDefault(); 759 if (cleaned) document.execCommand('insertText', false, cleaned); 760 } 761 }); 762 763 emailField.addEventListener('input', function () { 764 var v = this.value || ''; 765 var fixed = v.replace(/[^a-zA-Z0-9.!#$%&'*+\/=?^_`{|}~@-]/g, ''); 766 if (fixed !== v) this.value = fixed; 767 }); 768 emailField.addEventListener('blur', function () { 769 this.value = (this.value || '').trim(); 770 }); 771 } 772 773 var phoneInput = qs(root, 'input[type="tel"]'); 774 if (phoneInput) { 775 phoneInput.addEventListener('input', function () { 776 777 var value = this.value.replace(/[^0-9+\-\s\(\)]/g, ''); 778 this.value = value; 779 780 phoneInput.classList.remove('error'); 781 phoneInput.removeAttribute('aria-invalid'); 782 var fg = phoneInput.closest('.form-group'); 783 if (fg) fg.classList.remove('has-error'); 784 phoneInput.style.borderColor = ''; 785 phoneInput.style.boxShadow = ''; 786 }); 787 788 phoneInput.addEventListener('paste', function (e) { 789 e.preventDefault(); 790 var pastedText = (e.clipboardData || window.clipboardData).getData('text'); 791 var phoneOnly = pastedText.replace(/[^0-9+\-\s\(\)]/g, ''); 792 if (phoneOnly) { 793 this.value = phoneOnly; 794 } 795 }); 796 } 797 798 qsa(root, 'input.form-input').forEach(function (inp) { 799 inp.addEventListener('input', function () { 800 inp.classList.remove('error'); 801 inp.removeAttribute('aria-invalid'); 802 var fg = inp.closest('.form-group'); 803 if (fg) fg.classList.remove('has-error'); 804 inp.style.borderColor = ''; 805 inp.style.boxShadow = ''; 806 }); 807 }); 808 809 qsa(root, '.module-item').forEach(function (module) { 810 module.addEventListener('click', function (e) { 811 e.stopPropagation(); 812 e.preventDefault(); 813 814 // Store the previous state of selectedModules before updating 815 var previousModules = [...selectedModules]; 816 817 module.classList.toggle('selected'); 818 var moduleId = module.getAttribute('data-module'); 819 820 var isSelected = module.classList.contains('selected'); 821 if (moduleId === 'recruit' && jobSlotsGroup) { 822 jobSlotsGroup.style.display = isSelected ? 'block' : 'none'; 823 // Keep the typed value. Wiping it meant that unticking and 824 // reticking Recruit silently dropped the size, and the saving 825 // could not reappear until the visitor typed it again. 826 } 827 828 var staffSel = !!qs(root, '.module-item[data-module="staff"].selected'); 829 var boardingSel = !!qs(root, '.module-item[data-module="boarding"].selected'); 830 var wellbeingSel = !!qs(root, '.module-item[data-module="wellbeing"].selected'); 831 var complySel = !!qs(root, '.module-item[data-module="comply"].selected'); 832 var showEmployees = staffSel || boardingSel || wellbeingSel || complySel; 833 if (employeesGroup) employeesGroup.style.display = showEmployees ? 'block' : 'none'; 834 // Same here: the headcount is remembered while the field is hidden. 835 if (!isSuiteSelected) { 836 var recruitSel = !!qs(root, '.module-item[data-module="recruit"].selected'); 837 838 // Always update the input visibility through toggleInputs() 839 // This ensures consistent rules are applied everywhere 840 toggleInputs(); 841 842 checkAndShowContactSection(); 843 } 844 845 updateSelectedModules(); 846 847 // Make sure customModules stays visible 848 if (customModules) customModules.classList.add('show'); 849 }); 850 }); 851 852 var ctaButton = qs(root, '.cta-button'); 853 if (ctaButton) { 854 ctaButton.addEventListener('click', function (e) { 855 856 var requiredInputs = []; 857 858 var selectedIndex = options.findIndex(function (o) { 859 return o.classList.contains('selected'); 860 }); 861 var isSuiteSelected = (selectedIndex === 0); 862 863 var nameInput = qs(root, '#contact-name'); 864 var emailInput = qs(root, 'input[type="email"]'); 865 var phoneInput = qs(root, 'input[type="tel"]'); 866 if (nameInput) requiredInputs.push(nameInput); 867 if (emailInput) requiredInputs.push(emailInput); 868 if (phoneInput) requiredInputs.push(phoneInput); 869 870 var recruitSel = !!qs(root, '.module-item[data-module="recruit"].selected'); 871 var staffSel = !!qs(root, '.module-item[data-module="staff"].selected'); 872 if (isSuiteSelected || recruitSel) { 873 if (jobSlotsInput) requiredInputs.push(jobSlotsInput); 874 } 875 if (isSuiteSelected || staffSel) { 876 if (employeesInput) requiredInputs.push(employeesInput); 877 } 878 879 var hasError = false;
880 requiredInputs.forEach(function (input) { 881 // Skip validation if the input or its parent container is not visible 882 var isVisible = true; 883 var parent = input.parentNode; 884 885 // Check if this is a job slots or employees input and check visibility accordingly 886 var isJobSlotsOrEmployees = (input === jobSlotsInput || input === employeesInput); 887 888 if (isJobSlotsOrEmployees) { 889 // Check if we're in hidden inputs mode and the inputs aren't shown yet 890 var rootElement = document.querySelector('.hron-new-price-root'); 891 var inHideInputsMode = rootElement && rootElement.classList.contains('hron-hide-inputs'); 892 var inputsShown = rootElement && rootElement.classList.contains('show-inputs'); 893 894 if (inHideInputsMode && !inputsShown) { 895 // Skip validation for hidden inputs 896 isVisible = false; 897 } 898 899 // Also check if the specific input container is visible 900 while (parent && parent !== document.body) { 901 var style = window.getComputedStyle(parent); 902 if (style.display === 'none' || style.visibility === 'hidden') { 903 isVisible = false; 904 break; 905 } 906 parent = parent.parentNode; 907 } 908 } 909 910 // Skip validation for invisible elements 911 if (!isVisible) { 912 return; 913 } 914 915 // Check if this input was just revealed for the first time in hide_inputs mode 916 var justRevealed = input.dataset && input.dataset.justRevealed === 'true'; 917 918 // If the input was just revealed, clear the flag and skip validation for now 919 if (justRevealed) { 920 delete input.dataset.justRevealed; 921 return; 922 } 923 924 var val = (input.value || '').trim(); 925 var isEmail = input.type === 'email'; 926 927 var emailRegex = /^[a-zA-Z0-9.!#$%&'*+\/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$/; 928 var isNumberInput = input === jobSlotsInput || input === employeesInput; 929 var isPhone = input === phoneInput; 930 var ok = val !== '' && (!isEmail || emailRegex.test(val)); 931 if (isPhone) { 932 var d = val.replace(/\D+/g, ''); 933 if (d.length < 8) ok = false; 934 } 935 if (isNumberInput && (val === '0' || val === '' || isNaN(Number(val)) || Number(val) < 1)) { 936 ok = false; 937 938 } 939 if (!ok) { 940 input.classList.add('error'); 941 input.setAttribute('aria-invalid', 'true'); 942 var fg = input.closest('.form-group'); 943 if (fg) fg.classList.add('has-error'); 944 try { 945 input.style.borderColor = '#FF3B30'; 946 input.style.boxShadow = '0 0 0 4px rgba(255,59,48,0.12)'; 947 } catch (_) { } 948 hasError = true; 949 } else { 950 input.classList.remove('error'); 951 input.removeAttribute('aria-invalid'); 952 var fg2 = input.closest('.form-group'); 953 if (fg2) fg2.classList.remove('has-error'); 954 try { 955 input.style.borderColor = ''; 956 input.style.boxShadow = ''; 957 } catch (_) { } 958 } 959 }); 960 961 if (emailInput) { 962 emailInput.addEventListener('input', function (e) { 963 var allowed = /^[a-zA-Z0-9.!#$%&'*+\/=?^_`{|}~@-]*$/; 964 if (!allowed.test(this.value)) { 965 this.value = this.value.replace(/[^a-zA-Z0-9.!#$%&'*+\\/?^_`{|}~@-]/g, ''); 966 } 967 }); 968 } 969 970 if (hasError) { 971 e.preventDefault(); 972 e.stopPropagation(); 973 974 var firstInvalid = requiredInputs.find(function (inp) { 975 return inp.classList.contains('error'); 976 }); 977 if (firstInvalid) { 978 try { 979 firstInvalid.focus({ 980 preventScroll: true 981 }); 982 } catch (_) { 983 try { 984 firstInvalid.focus(); 985 } catch (_) { } 986 } 987 try { 988 firstInvalid.scrollIntoView({ 989 behavior: 'smooth', 990 block: 'center' 991 }); 992 } catch (_) { } 993 } 994 return false;
995 } 996 997 if (!hronWebhookFired) { 998 try { 999 1000 var selected = { 1001 staff: !!qs(root, '.module-item[data-module="staff"].selected'), 1002 recruit: !!qs(root, '.module-item[data-module="recruit"].selected'), 1003 boarding: !!qs(root, '.module-item[data-module="boarding"].selected'), 1004 wellbeing: !!qs(root, '.module-item[data-module="wellbeing"].selected'), 1005 comply: !!qs(root, '.module-item[data-module="comply"].selected') 1006 }; 1007 var currencyEl = qs(root, '.currency-option.selected'); 1008 var currency = currencyEl ? currencyEl.getAttribute('data-currency') : 'DKK'; 1009 var employeesVal = (selected.staff || selected.boarding || selected.wellbeing || selected.comply) && employeesInput ? (employeesInput.value || '').trim() : ''; 1010 var jobslotsVal = selected.recruit && jobSlotsInput ? (jobSlotsInput.value || '').trim() : ''; 1011 1012 var selectedSystem = (function () { 1013 // Only individual module selection exists now (no suite option) 1014 // Build string based on actually selected modules 1015 var mods = []; 1016 if (selected.recruit) mods.push('recruit'); 1017 if (selected.boarding) mods.push('boarding'); 1018 if (selected.staff) mods.push('staff'); 1019 if (selected.wellbeing) mods.push('wellbeing'); 1020 if (selected.comply) mods.push('comply'); 1021 return mods.length ? mods.join(',') : ''; 1022 })(); 1023 1024 calculatePrices(parseInt(employeesVal || '0') || 0, parseInt(jobslotsVal || '0') || 0, currency, selected) 1025 .finally(function () { 1026 var nameInput = qs(root, '#contact-name'); 1027 var emailInput = qs(root, '#contact-section input[type="email"]'); 1028 var phoneInput = qs(root, '#contact-section input[type="tel"]'); 1029 1030 var payload = { 1031 your_name: nameInput ? (nameInput.value || '').trim() : '', 1032 your_email: emailInput ? (emailInput.value || '').trim() : '', 1033 your_phone: phoneInput ? (phoneInput.value || '').trim() : '', 1034 theurl: window.location.href, 1035 selectedSystem: selectedSystem, 1036 employees: employeesVal, 1037 jobslots: jobslotsVal, 1038 currency: (window.HRONPriceModal && window.HRONPriceModal.currentCurrency) || currency, 1039 hiddenTextArea: '' 1040 }; 1041 1042 var utms = getUtmParams(); 1043 for (var k in utms) { 1044 if (Object.prototype.hasOwnProperty.call(utms, k)) { 1045 payload[k] = utms[k]; 1046 } 1047 } 1048 1049 var buildWebhookFd = function () { 1050 var fd = new FormData(); 1051 fd.append('action', HRONNewPrice.webhookAction); 1052 fd.append('nonce', HRONNewPrice.nonce); 1053 Object.keys(payload).forEach(function (key) { 1054 fd.append(key, payload[key]); 1055 }); 1056 return fd; 1057 }; 1058 1059 fetch(HRONNewPrice.ajaxUrl, { 1060 method: 'POST', 1061 body: buildWebhookFd(), 1062 credentials: 'same-origin' 1063 }) 1064 .then(function (r) { 1065 // Retry once on 403 (nonce expired) 1066 if (r.status === 403) { 1067 return refreshHRONNonce().then(function () { 1068 return fetch(HRONNewPrice.ajaxUrl, { 1069 method: 'POST', 1070 body: buildWebhookFd(), 1071 credentials: 'same-origin' 1072 }); 1073 }); 1074 } 1075 return r; 1076 }) 1077 .finally(function () { 1078 hronWebhookFired = true; 1079 }); 1080 }); 1081 } catch (_) { 1082 1083 hronWebhookFired = true; 1084 } 1085 } 1086 1087 if (window.HRONPriceModal && typeof window.HRONPriceModal.open === 'function') { 1088 window.HRONPriceModal.open(); 1089 } 1090 }); 1091 } 1092 1093 function calculatePrices(employees, jobslots, currency) { 1094 var selections = arguments[3]; 1095 var buildFormData = function () { 1096 var formData = new FormData(); 1097 formData.append('action', 'calculate_prices'); 1098 formData.append('nonce', HRONNewPrice.nonce); 1099 formData.append('employees', employees); 1100 formData.append('jobslots', jobslots); 1101 formData.append('currency', currency); 1102 formData.append('select_staff', selections && selections.staff ? '1' : '0'); 1103 formData.append('select_recruit', selections && selections.recruit ? '1' : '0'); 1104 formData.append('select_boarding', selections && selections.boarding ? '1' : '0'); 1105 formData.append('select_wellbeing', selections && selections.wellbeing ? '1' : '0'); 1106 formData.append('select_comply', selections && selections.comply ? '1' : '0'); 1107 return formData; 1108 }; 1109 1110 var mySeq = ++priceRequestSeq; 1111 1112 return new Promise(function (resolve, reject) { 1113 fetch(HRONNewPrice.ajaxUrl, { 1114 method: 'POST', 1115 body: buildFormData() 1116 }) 1117 .then(function (response) { 1118 // Retry once on 403 (nonce expired) 1119 if (response.status === 403) { 1120 return refreshHRONNonce().then(function () { 1121 return fetch(HRONNewPrice.ajaxUrl, { 1122 method: 'POST', 1123 body: buildFormData() 1124 }); 1125 }); 1126 } 1127 return response; 1128 }) 1129 .then(function (response) { 1130 return response.json(); 1131 }) 1132 .then(function (data) { 1133 if (data.success) { 1134 // Effective saving off standalone, straight from the 1135 // server so the badge and the modal agree. 1136 var rawTotals = data.data && data.data.totals && data.data.totals.raw 1137 ? data.data.totals.raw : null; 1138 var savedPct = null; 1139 if (rawTotals && typeof rawTotals.saved_percent === 'number') { 1140 savedPct = rawTotals.saved_percent; 1141 } else if (rawTotals && typeof rawTotals.effective_rate === 'number') { 1142 savedPct = rawTotals.effective_rate * 100; 1143 } 1144 // Ignore a response that a newer request has superseded. 1145 // A module's standalone price does not depend on the 1146 // rest of the selection, so cache these and reuse them. 1147 if (mySeq === priceRequestSeq && rawTotals && rawTotals.standalone_dkk) { 1148 standaloneCache = { 1149 employees: employees, 1150 jobslots: jobslots, 1151 prices: rawTotals.standalone_dkk 1152 }; 1153 } 1154 1155 if (mySeq === priceRequestSeq) { 1156 if (data.data && data.data.is_enterprise) { 1157 // Enterprise deals are quoted by hand, so there 1158 // is no standard saving to advertise. 1159 effectiveDiscountKnown = false;
1160 updateDiscountBadge(); 1161 } else if (typeof savedPct === 'number') { 1162 effectiveDiscountRate = Math.round(savedPct); 1163 effectiveDiscountKnown = true; 1164 updateDiscountBadge(); 1165 } 1166 } 1167 resolve(data.data); 1168 } else { 1169 reject(new Error('Price calculation failed')); 1170 } 1171 }) 1172 .catch(function (error) { 1173 reject(error); 1174 }); 1175 }); 1176 } 1177 1178 // Current module selection, read straight from the DOM. 1179 function readModuleSelection() { 1180 return { 1181 recruit: !!qs(root, '.module-item[data-module="recruit"].selected'), 1182 staff: !!qs(root, '.module-item[data-module="staff"].selected'), 1183 boarding: !!qs(root, '.module-item[data-module="boarding"].selected'), 1184 wellbeing: !!qs(root, '.module-item[data-module="wellbeing"].selected'), 1185 comply: !!qs(root, '.module-item[data-module="comply"].selected') 1186 }; 1187 } 1188 1189 // Sizes currently in the inputs, plus which of them this selection needs. 1190 function readSizes(selected) { 1191 var employeesEl = qs(root, '#employees-input'); 1192 var jobSlotsEl = qs(root, '#job-slots-input'); 1193 return { 1194 employees: employeesEl ? parseInt((employeesEl.value || '').trim(), 10) || 0 : 0, 1195 jobslots: jobSlotsEl ? parseInt((jobSlotsEl.value || '').trim(), 10) || 0 : 0, 1196 needsEmployees: !!(selected.staff || selected.boarding || selected.wellbeing || selected.comply), 1197 needsJobslots: !!selected.recruit 1198 }; 1199 } 1200 1201 // Blend the saving out of the cached standalone prices. Every module's 1202 // standalone price is independent of the rest of the deal, so this gives 1203 // the same figure the server would return, with no round-trip. 1204 // Returns null when the cache does not cover this selection. 1205 function savingFromCache(selected) { 1206 var sizes = readSizes(selected); 1207 var limits = (window.HRONNewPrice && HRONNewPrice.enterpriseLimits) || {}; 1208 if (sizes.employees > limits.employees || sizes.jobslots > limits.jobslots) return null; 1209 if ((sizes.needsEmployees && sizes.employees <= 0) || 1210 (sizes.needsJobslots && sizes.jobslots <= 0)) return null; 1211 1212 // Use the server's pricing configuration on the first render too. 1213 // This mirrors raw_module_dkk; no network round-trip is needed. 1214 var models = (window.HRONNewPrice && HRONNewPrice.modulePricing) || {}; 1215 var localPrices = {}; 1216 Object.keys(models).forEach(function (key) { 1217 var m = models[key]; 1218 var units = m.unit_of === 'jobslots' ? sizes.jobslots : sizes.employees; 1219 var curve = function (n) { return m.start + m.unit * Math.pow(n, 1 - m.d); }; 1220 var price = m.knee && units > m.knee 1221 ? curve(m.knee) * Math.pow(units / m.knee, 1 - m.d2) 1222 : curve(units); 1223 localPrices[key] = units > 0 ? Math.max(price, m.min_annual) : 0; 1224 }); 1225 if (Object.keys(models).length) { 1226 standaloneCache = { employees: sizes.employees, jobslots: sizes.jobslots, prices: localPrices }; 1227 } 1228 if (!standaloneCache || !standaloneCache.prices) return null; 1229 if (sizes.needsEmployees && standaloneCache.employees !== sizes.employees) return null; 1230 if (sizes.needsJobslots && standaloneCache.jobslots !== sizes.jobslots) return null; 1231 1232 var rates = (window.HRONNewPrice && HRONNewPrice.comboRates) || {}; 1233 var keys = [];
1234 Object.keys(selected).forEach(function (k) { if (selected[k]) keys.push(k); }); 1235 if (keys.length < 2) return 0; 1236 1237 var hasStaff = !!selected.staff; 1238 var full = 0, net = 0; 1239 for (var i = 0; i < keys.length; i++) { 1240 var k = keys[i]; 1241 var price = standaloneCache.prices[k]; 1242 if (!(price > 0)) return null; 1243 var r = rates[k] || {}; 1244 // Staff always uses its own rate; the others depend on whether 1245 // Staff is in the deal. Mirrors the server's combo_rate(). 1246 var pct = (k === 'staff') 1247 ? (+r.withoutStaff || 0) 1248 : (hasStaff ? (+r.withStaff || 0) : (+r.withoutStaff || 0)); 1249 full += price; 1250 net += price * (1 - pct / 100); 1251 } 1252 return full > 0 ? (1 - net / full) * 100 : 0; 1253 } 1254 1255 // Repaint the badge immediately, before any request leaves the page. 1256 function updateDiscountNow() { 1257 // Invalidate pending responses immediately, including the debounce window. 1258 priceRequestSeq++; 1259 var selected = readModuleSelection(); 1260 window.HRONUpdateComboBadges(root, selected, readSizes(selected)); 1261 var count = 0; 1262 Object.keys(selected).forEach(function (k) { if (selected[k]) count++; }); 1263 1264 // A single module never gets a combo discount. 1265 if (count < 2) { 1266 effectiveDiscountRate = 0; 1267 effectiveDiscountKnown = true; 1268 updateDiscountBadge(); 1269 return; 1270 } 1271 1272 var pct = savingFromCache(selected); 1273 if (pct === null) { 1274 // Nothing cached for these sizes yet, so wait for the request 1275 // rather than show a figure we cannot stand behind. 1276 effectiveDiscountKnown = false; 1277 updateDiscountBadge(); 1278 return; 1279 } 1280 effectiveDiscountRate = Math.round(pct); 1281 effectiveDiscountKnown = true; 1282 updateDiscountBadge(); 1283 } 1284 1285 // Confirms the instant figure and refreshes the cache after a size change. 1286 function refreshDiscountRate() { 1287 var selected = readModuleSelection(); 1288 var count = 0; 1289 Object.keys(selected).forEach(function (k) { if (selected[k]) count++; }); 1290 1291 // A single module never gets a combo discount, so there is nothing to fetch. 1292 if (count < 2) { 1293 effectiveDiscountRate = 0; 1294 effectiveDiscountKnown = true; 1295 updateDiscountBadge(); 1296 return; 1297 } 1298 1299 var sizes = readSizes(selected); 1300 1301 // Every selected module needs its own size before the saving means 1302 // anything. Until then stay quiet rather than show a partial figure. 1303 if ((sizes.needsEmployees && sizes.employees <= 0) || 1304 (sizes.needsJobslots && sizes.jobslots <= 0)) { 1305 effectiveDiscountKnown = false; 1306 updateDiscountBadge(); 1307 return; 1308 } 1309 1310 var currencyEl = qs(root, '.currency-option.selected'); 1311 var currency = currencyEl ? currencyEl.getAttribute('data-currency') : 'DKK'; 1312 1313 // Send both sizes as entered so the cached prices cover every module, 1314 // not just the ones ticked right now. 1315 var pending = calculatePrices(sizes.employees, sizes.jobslots, currency, selected); 1316 var refreshSeq = priceRequestSeq; 1317 pending.catch(function () { 1318 if (refreshSeq !== priceRequestSeq) return; 1319 effectiveDiscountKnown = false;
1320 updateDiscountBadge(); 1321 }); 1322 } 1323 1324 // Debounced so typing a headcount does not fire a request per keystroke. 1325 // The badge itself has already repainted via updateDiscountNow(). 1326 function scheduleDiscountRefresh(delay) { 1327 if (discountRefreshTimer) window.clearTimeout(discountRefreshTimer); 1328 discountRefreshTimer = window.setTimeout( 1329 refreshDiscountRate, 1330 typeof delay === 'number' ? delay : 300 1331 ); 1332 } 1333 1334 function updateSelectedModules() { 1335 1336 if (isSuiteSelected) { 1337 selectedModules = ['staff', 'recruit', 'boarding', 'wellbeing', 'comply']; 1338 } else { 1339 // Store the previous modules before updating 1340 var previousModules = [...selectedModules]; 1341 1342 // Update the current selected modules 1343 selectedModules = []; 1344 qsa(root, '.module-item.selected').forEach(function (module) { 1345 var moduleId = module.getAttribute('data-module'); 1346 if (moduleId) { 1347 selectedModules.push(moduleId); 1348 } 1349 }); 1350 1351 // Find removed modules (ones that were in previous but not in current) 1352 var removedModules = previousModules.filter(function (mod) { 1353 return !selectedModules.includes(mod); 1354 }); 1355 1356 // Find added modules (ones that are in current but weren't in previous) 1357 var addedModules = selectedModules.filter(function (mod) { 1358 return !previousModules.includes(mod); 1359 }); 1360 1361 if (removedModules.length > 0) { 1362 // Update mini-module icons with removal animation 1363 updateMiniModuleIcons(removedModules, 'remove'); 1364 } 1365 1366 if (addedModules.length > 0) { 1367 // Update mini-module icons with add animation 1368 updateMiniModuleIcons(addedModules, 'add'); 1369 } 1370 } 1371 1372 // Repaint the saving straight away from the cached standalone prices, 1373 // then confirm it with the server in the background. 1374 updateDiscountNow(); 1375 scheduleDiscountRefresh(); 1376 1377 // Update icon visibility in the top image 1378 if (typeof window.updateIconsVisibility === 'function') { 1379 window.updateIconsVisibility(); 1380 } 1381 } 1382 1383 function updateMiniModuleIcons(modules, action) { 1384 var miniModuleContainer = qs(root, '.mini-module-icons'); 1385 if (!miniModuleContainer) { 1386 return; 1387 } 1388 1389 var miniModuleIcons = qsa(miniModuleContainer, '.mini-module-icon'); 1390 1391 // Store module info for potential use 1392 var moduleImages = { 1393 'recruit': 'price-logo-recruit.svg', 1394 'boarding': 'price-logo-boarding.svg', 1395 'staff': 'price-logo-staff.svg', 1396 'wellbeing': 'price-logo-wellbeing.svg', 1397 'comply': 'price-logo-comply.svg' 1398 }; 1399 1400 var moduleTitles = { 1401 'recruit': 'Recruit', 1402 'boarding': 'Onboard', 1403 'staff': 'Staff', 1404 'wellbeing': 'Thrive', 1405 'comply': 'Comply' 1406 }; 1407 1408 // Default to remove if no action specified 1409 action = action || 'remove'; 1410 1411 if (modules && modules.length) { 1412 if (action === 'remove') { 1413 // Handle removed module animations 1414 1415 modules.forEach(function (moduleId) { 1416 var iconToRemove = qs(miniModuleContainer, '.mini-module-icon[data-module="' + moduleId + '"]'); 1417 if (iconToRemove) { 1418 var parentDiv = iconToRemove; 1419 1420 if (parentDiv) { 1421 // Get position before removing 1422 var rect = parentDiv.getBoundingClientRect(); 1423 1424 // Clone the element for animation 1425 var clone = parentDiv.cloneNode(true); 1426 document.body.appendChild(clone); 1427 1428 // Apply animation style to clone 1429 clone.style.position = 'fixed'; 1430 clone.style.left = rect.left + 'px'; 1431 clone.style.top = rect.top + 'px'; 1432 clone.style.width = rect.width + 'px'; 1433 clone.style.height = rect.height + 'px'; 1434 clone.style.transition = 'all 0.6s ease-out'; 1435 clone.style.zIndex = '10000'; 1436 1437 // Start animation 1438 setTimeout(function () { 1439 clone.style.transform = 'translateY(-30px)'; 1440 clone.style.opacity = '0'; 1441 }, 10); 1442 1443 // Remove after animation completes 1444 setTimeout(function () { 1445 clone.remove(); 1446 }, 650); 1447 1448 // Remove original icon 1449 parentDiv.remove(); 1450 } 1451 } 1452 }); 1453 } else if (action === 'add') { 1454
1455 modules.forEach(function (moduleId) { 1456 // Check if the icon already exists 1457 if (qs(miniModuleContainer, '.mini-module-icon[data-module="' + moduleId + '"]')) { 1458 return; 1459 } 1460 1461 // Find the correct position to insert 1462 var iconOrder = ['staff', 'recruit', 'boarding', 'wellbeing', 'comply']; 1463 var currentModules = selectedModules.slice().sort(function (a, b) { 1464 return iconOrder.indexOf(a) - iconOrder.indexOf(b); 1465 }); 1466 1467 var insertIndex = currentModules.indexOf(moduleId); 1468 1469 // Create new icon 1470 var newIcon = document.createElement('div'); 1471 newIcon.className = 'mini-module-icon'; 1472 newIcon.setAttribute('data-module', moduleId); 1473 1474 // Styling 1475 newIcon.style.border = 'solid 2px #dbdbdb'; 1476 newIcon.style.width = '24px'; 1477 newIcon.style.height = '24px'; 1478 newIcon.style.borderRadius = '6px'; 1479 newIcon.style.overflow = 'hidden'; 1480 newIcon.style.backgroundColor = 'white'; 1481 newIcon.style.position = 'relative'; 1482 newIcon.style.boxShadow = '0 1px 3px rgba(0,0,0,0.1)'; 1483 newIcon.style.opacity = '0'; 1484 newIcon.style.transform = 'translateY(-30px)'; 1485 newIcon.style.transition = 'all 0.6s ease-out'; 1486 1487 // Create image element 1488 var img = document.createElement('img'); 1489 img.src = window.HRONNewPrice && window.HRONNewPrice.pluginUrl ? 1490 (window.HRONNewPrice.pluginUrl + 'assets/img/' + moduleImages[moduleId]) : 1491 (pluginUrl + 'assets/img/' + moduleImages[moduleId]); 1492 img.alt = moduleTitles[moduleId] || moduleId; 1493 img.style.width = '100%'; 1494 img.style.height = '100%'; 1495 img.style.objectFit = 'contain'; 1496 1497 newIcon.appendChild(img); 1498 1499 // Find where to insert the new icon 1500 if (insertIndex === 0 || miniModuleIcons.length === 0) { 1501 // Insert at beginning 1502 miniModuleContainer.insertBefore(newIcon, miniModuleContainer.firstChild); 1503 } else if (insertIndex >= miniModuleIcons.length) { 1504 // Insert at end 1505 miniModuleContainer.appendChild(newIcon); 1506 } else { 1507 // Insert at specific position 1508 var referenceNode = miniModuleIcons[insertIndex]; 1509 if (referenceNode) { 1510 miniModuleContainer.insertBefore(newIcon, referenceNode); 1511 } else { 1512 miniModuleContainer.appendChild(newIcon); 1513 } 1514 } 1515 1516 // Animate the icon appearing 1517 setTimeout(function () { 1518 newIcon.style.opacity = '1'; 1519 newIcon.style.transform = 'translateY(0)'; 1520 }, 10); 1521 }); 1522 } 1523 1524 // Reorder all icons after the animation starts 1525 setTimeout(function () { 1526 var allIcons = qsa(miniModuleContainer, '.mini-module-icon'); 1527 1528 allIcons.forEach(function (icon, index) { 1529 var leftOffset = index * (-3); // Same as original layout 1530 icon.style.transition = 'all 0.5s ease'; 1531 icon.style.marginLeft = index > 0 ? leftOffset + 'px' : '0'; 1532 icon.style.marginRight = (index == allIcons.length - 2) ? '2px' : '-2px'; 1533 icon.style.zIndex = (index + 1).toString(); 1534 }); 1535 }, 150); 1536 } 1537 } 1538 1539 // Cancel any count-up or pulse still in flight. Without this, fast 1540 // toggling left several animations running at once and whichever 1541 // finished last won, so the pill could settle on a stale number. 1542 function stopDiscountAnimation() { 1543 if (discountAnimFrame !== null) { 1544 if (window.cancelAnimationFrame) window.cancelAnimationFrame(discountAnimFrame); 1545 discountAnimFrame = null; 1546 } 1547 if (discountPulseTimer !== null) { 1548 window.clearTimeout(discountPulseTimer); 1549 discountPulseTimer = null; 1550 } 1551 } 1552 1553 // displayedDiscountRate is the single source of truth for what the pill 1554 // shows. Reading it back out of the DOM mid-animation is what made the 1555 // old version drift. 1556 function setDiscountText(el, value) { 1557 displayedDiscountRate = value; 1558 el.textContent = String(value); 1559 } 1560 1561 function animateDiscountTo(el, target, duration) { 1562 stopDiscountAnimation(); 1563 1564 // Nothing on screen yet: land on the figure rather than counting up 1565 // from the placeholder in the markup. 1566 if (displayedDiscountRate === null || displayedDiscountRate === target) { 1567 setDiscountText(el, target); 1568 return; 1569 } 1570 1571 var from = displayedDiscountRate; 1572 var startTime = null; 1573 function step(timestamp) { 1574 if (startTime === null) startTime = timestamp; 1575 var progress = Math.min((timestamp - startTime) / duration, 1); 1576 setDiscountText(el, Math.round(from + (target - from) * progress)); 1577 if (progress < 1) { 1578 discountAnimFrame = window.requestAnimationFrame(step); 1579 } else { 1580 discountAnimFrame = null; 1581 setDiscountText(el, target); 1582 } 1583 } 1584 discountAnimFrame = window.requestAnimationFrame(step); 1585 } 1586 1587 function updateDiscountBadge() { 1588 var discountBtn = qs(root, '.new-price-discount-btn'); 1589 var discountRateText = qs(root, '#new-price-discount'); 1590 if (!discountBtn || !discountRateText) return; 1591 1592 var customChip = discountBtn.closest('.custom-chip'); 1593 var discountLabel = qs(root, '.custom-chip .discount-label'); 1594 1595 // Count from the DOM, not from the cached selectedModules array, so 1596 // this can never disagree with what the visitor has t
1596icked. 1597 var selected = readModuleSelection(); 1598 var moduleCount = 0; 1599 Object.keys(selected).forEach(function (k) { if (selected[k]) moduleCount++; }); 1600 1601 var comboRates = (window.HRONNewPrice && HRONNewPrice.comboRates) || {}; 1602 var allModulesSelected = moduleCount >= Object.keys(comboRates).length; 1603 var discountRate = effectiveDiscountRate; 1604 1605 // A figure is shown only when one exists: two or more modules, a 1606 // saving already computed, and that saving above zero. 1607 var hasRealDiscount = moduleCount >= 2 && effectiveDiscountKnown && discountRate > 0; 1608 1609 // The chip also carries the mini module icons and the label, so its 1610 // visibility follows the module count, not the saving. 1611 if (customChip) { 1612 customChip.style.display = moduleCount < 1 ? 'none' : 'flex'; 1613 } 1614 1615 if (!hasRealDiscount) { 1616 // Stop mid-flight animations and forget the shown value, so the 1617 // pill starts clean the next time a saving applies. 1618 stopDiscountAnimation(); 1619 displayedDiscountRate = null; 1620 discountBtn.hidden = true; 1621 discountBtn.style.display = 'none'; 1622 if (discountLabel) discountLabel.style.display = 'none'; 1623 return; 1624 } 1625 1626 discountBtn.hidden = false; 1627 discountBtn.style.display = ''; 1628 if (discountLabel) discountLabel.style.display = ''; 1629 1630 if (displayedDiscountRate === discountRate) { 1631 stopDiscountAnimation(); 1632 return; 1633 } 1634 1635 // Kept short: the figure is already correct, this is just polish. 1636 animateDiscountTo(discountRateText, discountRate, 320); 1637 1638 if (allModulesSelected) { 1639 discountPulseTimer = window.setTimeout(function () { 1640 discountPulseTimer = null; 1641 animateDiscountButton(discountBtn); 1642 }, 380); 1643 } 1644 } 1645 1646 function updatePrices(currency) { 1647 if (currency === currentCurrency) return; 1648 1649 currentCurrency = currency; 1650 var rate = currency === 'EUR' ? currencyRate : 1; 1651 1652 qsa(root, '.module-price').forEach(function (priceEl) { 1653 priceEl.remove(); 1654 }); 1655 1656 qsa(root, '.hron-price').forEach(function (priceEl) { 1657 var priceText = priceEl.textContent; 1658 if (!originalPrices[priceEl]) { 1659 originalPrices[priceEl] = priceText; 1660 } 1661 1662 var originalText = originalPrices[priceEl]; 1663 1664 var match = originalText.match(/(?:^fra\s+)?(\d+)kr\/mdr/i); 1665 if (match) { 1666 var dkkAmount = parseInt(match[1]); 1667 var convertedAmount = currency === 'EUR' ? 1668 Math.round(dkkAmount * rate) : dkkAmount; 1669 var currencySuffix = currency === 'EUR' ? 'â¬' : 'kr'; 1670 1671 priceEl.textContent = 'fra ' + convertedAmount + currencySuffix + '/mdr'; 1672 } 1673 }); 1674 1675 var employeesInput = qs(root, '#employees-input'); 1676 var jobslotsInput = qs(root, '#job-slots-input'); 1677 1678 var employees = parseInt(employeesInput ? employeesInput.value : 0) || 0; 1679 var jobslots = parseInt(jobslotsInput ? jobslotsInput.value : 0) || 0; 1680 1681 var selected = { 1682 recruit: !!qs(root, '.module-item[data-module="recruit"].selected'), 1683 boarding: !!qs(root, '.module-item[data-module="boarding"].selected'), 1684 staff: !!qs(root, '.module-item[data-module="staff"].selected'), 1685 wellbeing: !!qs(root, '.module-item[data-module="wellbeing"].selected'), 1686 comply: !!qs(root, '.module-item[data-module="comply"].selected') 1687 }; 1688 1689 if (employees > 0 || jobslots > 0) { 1690 calculatePrices(employees, jobslots, currency, selected) 1691 .then(function (prices) { 1692 updatePriceDisplay(prices); 1693 }) 1694 .catch(function (error) { 1695 1696 }); 1697 } 1698 } 1699 1700 function updatePriceDisplay(prices) { 1701 1702 if (window.HRONPriceModal && typeof window.HRONPriceModal.updateModalPrices === 'function') { 1703 window.HRONPriceModal.updateModalPrices(); 1704 } 1705 } 1706 1707 var currencyIndicator = qs(root, '.currency-indicator'); 1708 if (currencyIndicator) { 1709 var selectedCurrency = qs(root, '.selected-currency'); 1710 var currencyOptions = qsa(root, '.currency-option'); 1711 currencyIndicator.addEventListener('click', function (e) { 1712 e.stopPropagation(); 1713 currencyIndicator.classList.toggle('open'); 1714 }); 1715 currencyOptions.forEach(function (option) { 1716 option.addEventListener('click', function (e) { 1717 e.stopPropagation(); 1718 var currency = option.getAttribute('data-currency'); 1719 1720 if (selectedCurrency) selectedCurrency.textContent = (currency === 'DKK' ? 'DKK' : currency); 1721 1722 currencyOptions.forEach(function (o) { 1723 o.classList.remove('selected'); 1724 }); 1725 option.classList.add('selected'); 1726 1727 updatePrices(currency); 1728 1729 setTimeout(function () { 1730 currencyIndicator.classList.remove('open'); 1731 }, 100); 1732 }); 1733 }); 1734 document.addEventListener('click', function () { 1735 currencyIndicator.classList.remove('open'); 1736 }); 1737 } 1738 1739 var employeesInput = qs(root, '#employees-input'); 1740 var jobslotsInput = qs(root, '#job-slots-input'); 1741 1742 if (employeesInput) { 1743 employeesInput.addEventListener('input', function () { 1744 var currency = qs(root, '.currency-option.selected'); 1745 var currentCurrency = currency ? currency.getAttribute('data-currency') : 'DKK'; 1746 updatePrices(currentCurrency); 1747 updateDiscountNow(); 1748 scheduleDiscountRefresh(); 1749 }); 1750 } 1751 1752 if (jobslotsInput) { 1753 jobslotsInput.addEventListener('input', function () { 1754 var currency = qs(root, '.currency-option.selected'); 1755 var currentCurrency = currency ? currency.getAttribute('data-currency') : 'DKK'; 1756 updatePrices(currentCurrency); 1757 updateDiscountNow(); 1758 scheduleDiscountRefresh(); 1759 }); 1760 } 1761 1762 var currencyIndicatorEl = qs(root, '.currency-indicator'); 1763 if (currencyIndicatorEl) { 1764 currencyIndicatorEl.style.display = ''; 1765 } 1766 1767 function clearLoading() { 1768 root.classList.remove('is-loading'); 1769 } 1770 clearLoading(); 1771 updateDiscountNow(); 1772 window.requestAnimationFrame(clearLoading); 1773 setTimeout(clearLoading, 100); 1774 1775 // Make sure custom modules are shown on page load 1776 setTimeout(function () { 1777 if (customModules) { 1778 customModules.classList.add('show'); 1779 1780 // Add click handler to prevent clicks from propagating through custom modules 1781 customModules.addEventListener('click', function (e) { 1782 e.stopPropagation(); 1783 }); 1784 } 1785 if (suiteInputs) suiteInputs.classList.remove('hide'); 1786 1787 // Initialize icon visibility based on selected modules 1788 if (typeof window.updateIconsVisibility === 'function') { 1789 window.updateIconsVisibility(); 1790 } 1791 1792 // Initialize discount badge with animation 1793 updateDiscountBadge(); 1794 1795 // Warm the standalone-price cache so the very first module toggle 1796 // repaints instantly instead of waiting for a round-trip. 1797 scheduleDiscountRefresh(0); 1798 }, 200); 1799 1800 // Function to animate the discount button with pulse and glow effect 1801 function animateDiscountButton(discountBtn) { 1802 // Prevent multiple animations from running at the same time 1803 if (discountBtn.dataset.animating === 'true') return; 1804 1805 discountBtn.dataset.animating = 'true'; 1806 1807 // Store original styles to restore later 1808 const originalTransition = discountBtn.style.transition; 1809 const originalTransform = discountBtn.style.transform; 1810 1811 // Add CSS for the scale animation 1812 discountBtn.style.transition = 'all 0.6s cubic-bezier(0.175, 0.885, 0.32, 1.275)'; 1813 discountBtn.style.transform = 'scale(1.15)'; 1814 1815 // Add the glow animation class 1816 discountBtn.classList.add('discount-btn-pulse'); 1817 1818 // Revert back after a delay 1819 setTimeout(() => { 1820 discountBtn.style.transform = 'scale(1)'; 1821 1822 // Reset the animating flag and restore original transition 1823 setTimeout(() => { 1824 discountBtn.style.transition = originalTransition; 1825 discountBtn.classList.remove('discount-btn-pulse'); 1826 discountBtn.dataset.animating = 'false'; 1827 }, 600); 1828 }, 800); 1829 } 1830 1831 // --------------------------------------------------------------- 1832 // Kiosk reset: the fair/exhibition screen never reloads, so when the 1833 // price panel is closed we must wipe everything the previous visitor
1834 // entered and re-arm the one-shot webhook guard, exactly as a fresh 1835 // page load would. Exposed globally so the kiosk shell can call it. 1836 // --------------------------------------------------------------- 1837 function resetPriceForm() { 1838 // Re-arm the one-shot webhook so the next visitor's submission 1839 // sends again (this is the "limit" that otherwise sticks). 1840 hronWebhookFired = false; 1841 isSuiteSelected = false; 1842 1843 // Clear every text / number / contact input and its visual state. 1844 qsa(root, 'input').forEach(function (inp) { 1845 var t = (inp.type || '').toLowerCase(); 1846 if (t === 'button' || t === 'submit' || t === 'hidden' || t === 'checkbox' || t === 'radio') return; 1847 inp.value = ''; 1848 inp.classList.remove('error'); 1849 inp.removeAttribute('aria-invalid'); 1850 inp.style.borderColor = ''; 1851 inp.style.boxShadow = ''; 1852 var fg = inp.closest('.form-group'); 1853 if (fg) { fg.classList.remove('has-error'); fg.classList.remove('has-value'); } 1854 }); 1855 1856 // Switch pricing back to DKK if the visitor changed currency. 1857 var dkkOpt = qs(root, '.currency-option[data-currency="DKK"]'); 1858 if (dkkOpt && !dkkOpt.classList.contains('selected')) { 1859 qsa(root, '.currency-option').forEach(function (o) { o.classList.remove('selected'); }); 1860 dkkOpt.classList.add('selected'); 1861 var selCur = qs(root, '.selected-currency'); 1862 if (selCur) selCur.textContent = 'DKK'; 1863 updatePrices('DKK'); 1864 } 1865 var curInd = qs(root, '.currency-indicator'); 1866 if (curInd) curInd.classList.remove('open'); 1867 1868 // Re-select every module (default = all four selected); the helper 1869 // restores the mini icons + discount badge for the re-added ones. 1870 qsa(root, '.module-item').forEach(function (m) { m.classList.add('selected'); }); 1871 updateSelectedModules(); 1872 1873 // Re-evaluate input/contact visibility for the default state. 1874 toggleInputs(); 1875 checkAndShowContactSection(); 1876 1877 // Restore the blue "enter number of employees" gate exactly as it 1878 // appears on a fresh load. The visitor types here first, and once 1879 // they continue the plugin hides this overlay permanently â so on a 1880 // never-reloading kiosk we must clear its value and show it again. 1881 var blurInput = document.getElementById('hron-blur-employee-input'); 1882 if (blurInput) { 1883 blurInput.value = ''; 1884 blurInput.classList.remove('error'); 1885 blurInput.style.borderColor = ''; 1886 blurInput.style.boxShadow = ''; 1887 } 1888 var blurBtn = document.getElementById('hron-blur-unlock-btn'); 1889 if (blurBtn) blurBtn.classList.remove('visible'); 1890 var blurOverlay = document.getElementById('hron-blur-overlay'); 1891 if (blurOverlay) { 1892 blurOverlay.style.display = ''; 1893 blurOverlay.classList.remove('fade-out'); 1894 } 1895 var recBanner = document.getElementById('hron-recommendation-banner'); 1896 if (recBanner) recBanner.classList.remove('visible', 'hiding'); 1897 } 1898 window.HRONResetPriceForm = resetPriceForm; 1899 }); 1900 }); 1901})();
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.