/* ========================================================================== healthretail.tbci.ge — functional part (Individual Medical Product) Written following the pattern of the old CASCO project (fetch/GetPolicyInfo, sendsmsmail, popup handling) — the old file itself was never copied in, it was only used as a reference for how things were wired up. All markup/ classes/ids below were verified directly against the real page https://tbc-avtomini-production.webflow.io/main via the live DOM. UPDATE (real methods received — see "2 methods.docx"): DATA_URL_TEST and SMS_URL_TEST below are now the real endpoints and REQ_PARAMS carries the real SecretPassword, with camelCase field names throughout. UPDATE 2 (CONFIRMED by TBC): GetPolicyInfo's response is wrapped, like the old CASCO project — {description, status, isSuccess, policyInfo: {...the actual fields}} — not flat as first assumed from the doc example. loadData() gates on isSuccess + policyInfo and unwraps policyInfo before handing it to processData(). UPDATE 3 (CONFIRMED by TBC, Kote 03.09.2026): test vs prod is no longer a manual flag — it's auto-detected from the hostname (*.webflow.io staging domain => test, any other/custom domain => prod), and prod uses its own SecretPassword (different from test's), also confirmed by TBC. See isProduction / SECRET_PASSWORD below. DATA_URL_PROD / SMS_URL_PROD are still inferred by the same test->prod domain swap used everywhere else on this project (myprofile-api-test -> myprofile-api-prod) — not explicitly confirmed by TBC, double-check before going live. Map of DOM hooks actually present on the page (verified in the browser): ---------------------------------------------------------------------- [contrahentfirstname] -> text = data.firstName [instalmentnumber] (2x: main block -> text = monthPremium + currency and .modal_component-buy-policy) .faq-item-2-table (7x, one per -> show .table-info-first, service category) hide .table-info-second when productId == 13891, and the other way round for 13990 [policywordingurl] -> text = data.wordingName, (.c-policy-popup__chb__label-link) href/target = data.policyWordingUrl [policywordingurlparameter] -> href = data.policyWordingUrl (.is-link-style, "სადაზღვევო გამონაკლისები და სრული პირობები") [data-gdpr-check] (2x: #buyPolicyBtnTwo -> click -> handleBuyClick() and the button inside .modal_component-buy-policy) -> when firstPaymentIsValid === true, both get class "is-disable-btn" added (styling for it lives in Webflow) + pointer-events:none .s-loading (class is--active) -> preloader until the API responds .s-error-not-found (class open) -> "შეთავაზება ვერ მოიძებნა" screen .c-popups-holder (class edit-mode) -> wrapper for the terms-consent popup .c-policy-popup__chb -> consent checkbox (label.w-checkbox) .c-policy-popup__chb__label (the div with the text, a SIBLING of the checkbox label — not to be confused with the empty span.c-policy-popup__chb__label.w-form-label) .c-policy-popup__cta [policynumberlinkaccepted] -> "შეიძინე" button inside the popup .loading-animation-black_component -> hidden by default; shown only while the sendsmsmail request (triggered by "შეიძინე") is in flight .c-policy-popup__close -> close popup (X) .c-popups-holder__bg -> close popup (click on backdrop) #show-popup + .modal_component-buy-policy.open -> sticky buy popup on scroll (stays visible while below the section, hides when scrolling back up to it — already agreed on earlier). Not used at all when firstPaymentIsValid === true — forced hidden instead. .modal_component (+ .open) -> "გადმოწერე აპლიკაცია" popup (QR) .modal_close-button -> close the app-download modal ========================================================================== */ var Webflow = Webflow || []; Webflow.push(function () { // ------------------------------------------------------------------ // Config // ------------------------------------------------------------------ // Auto-detected, not a manual flag: per TBC ("якщо урла стейдж то все // працює для тесту, якщо урла лайв то все працює для продакшена") — any // *.webflow.io staging URL uses test endpoints/credentials, any other // (real/custom) domain uses production ones. This also means we no longer // need to remember to flip a boolean before each release. const isProduction = !window.location.hostname.endsWith('.webflow.io'); const DATA_URL_TEST = 'https://myprofile-api-test.remoteapi.ge/myprofileportal/api/Health/GetPolicyInfo'; // TODO: confirmed test URL above; prod URL below is inferred (test -> prod domain swap), verify before go-live const DATA_URL_PROD = 'https://myprofile-api-prod.remoteapi.ge/myprofileportal/api/Health/GetPolicyInfo'; const DATA_URL = isProduction ? DATA_URL_PROD : DATA_URL_TEST; const SMS_URL_TEST = 'https://myprofile-api-test.remoteapi.ge/myprofileportal/api/Health/SendSMSEmail'; // TODO: confirmed test URL above; prod URL below is inferred (test -> prod domain swap), verify before go-live const SMS_URL_PROD = 'https://myprofile-api-prod.remoteapi.ge/myprofileportal/api/Health/SendSMSEmail'; const SMS_URL = isProduction ? SMS_URL_PROD : SMS_URL_TEST; const PAY_URL_TEST = 'https://on-test.tbci.ge/pay?policy='; const PAY_URL_PROD = 'https://on.tbci.ge/pay?policy='; const PAY_URL = isProduction ? PAY_URL_PROD : PAY_URL_TEST; // Credentials also differ test vs prod — confirmed by TBC (Kote, 03.09.2026): // prod SecretPassword is different from the test one. const SECRET_PASSWORD_TEST = 'E3@`{L-Mf%dx'; const SECRET_PASSWORD_PROD = '!DH2Zfdje^-$Ka2>VZV9'; const SECRET_PASSWORD = isProduction ? SECRET_PASSWORD_PROD : SECRET_PASSWORD_TEST; const urlParams = new URLSearchParams(window.location.search); const policyId = urlParams.has('pid') ? urlParams.get('pid') : ''; const contrahentId = urlParams.has('cid') ? urlParams.get('cid') : ''; const REQ_PARAMS = { SecretUserName: 'landingInfo', SecretPassword: SECRET_PASSWORD, Requester: 'MyProfilePortal', UserId: null, SessionID: '559e670a22874755b351a465dccf4134', PolicyId: policyId, ContrahentID: contrahentId, }; // ------------------------------------------------------------------ // Start // ------------------------------------------------------------------ initModals(); // "terms consent" popup — wire up handlers right away initAppModal(); // "გადმოწერე აპლიკაცია" popup — doesn't depend on API data loadData(); // ------------------------------------------------------------------ // Data loading // ------------------------------------------------------------------ async function loadData() { setLoading(true); let data; try { const resp = await fetch(DATA_URL, { method: 'POST', headers: { Accept: 'application/json', 'Content-Type': 'application/json' }, body: JSON.stringify(REQ_PARAMS), }); const text = await resp.text(); try { data = JSON.parse(text); console.log('[medical-page] DATA:', data); } catch (parseErr) { // Confirmed live on prod (healthretail.tbci.ge): for a PolicyId that // doesn't exist there, the server replies with a plain-text body // (e.g. "Offer Not Found"), not JSON — this is that endpoint's real // "not found" shape, not a network/parsing bug. Logged separately // from the network-error case below so a genuine failure is still // easy to tell apart in the console. console.log('[medical-page] DATA (non-JSON, treated as not found):', text); setLoading(false); showNotFound(); return; } } catch (e) { console.warn('[medical-page] fetch error:', e); setLoading(false); showNotFound(); return; } // CONFIRMED by TBC: the real response shape is wrapped, like the old // CASCO project — {description, status, isSuccess, policyInfo: {...the // actual fields: policyId, policyNumber, firstName, isValid, // firstPaymentIsValid, ...}}. This is the only shape we support now — // gate on isSuccess + policyInfo being present, matching the confirmed // contract exactly. if (data && data.isSuccess === true && data.policyInfo) { processData(data.policyInfo); } else { showNotFound(); } setLoading(false); } function setLoading(show) { const el = document.querySelector('.s-loading'); if (el) el.classList.toggle('is--active', show); } function showNotFound() { const el = document.querySelector('.s-error-not-found'); if (el) el.classList.add('open'); document.body.style.overflow = 'hidden'; // block page scroll while this screen is up } // ------------------------------------------------------------------ // Populating the page with data // ------------------------------------------------------------------ function processData(data) { // ---- name ---- document.querySelectorAll('[contrahentfirstname]').forEach(el => { el.textContent = data.firstName || ''; }); // ---- monthly premium + currency ---- const CURRENCY = data.grossWrittenPremiumCurrency === 1 ? '₾' : data.grossWrittenPremiumCurrency === 2 ? '$' : data.grossWrittenPremiumCurrency === 3 ? '€' : ''; const premiumText = `${new Intl.NumberFormat('fr-FR').format(data.monthPremium)} ${CURRENCY}`; document.querySelectorAll('[instalmentnumber]').forEach(el => { el.textContent = premiumText; }); // ---- services table: show the right product, hide the other one ---- const PRODUCT_A = 13891; const PRODUCT_B = 13990; document.querySelectorAll('.faq-item-2-table').forEach(block => { const first = block.querySelector('.table-info-first'); // product 13891 const second = block.querySelector('.table-info-second'); // product 13990 if (!first || !second) return; if (Number(data.productId) === PRODUCT_A) { first.style.display = ''; second.style.display = 'none'; } else if (Number(data.productId) === PRODUCT_B) { first.style.display = 'none'; second.style.display = ''; } else { console.warn('[medical-page] unknown product id:', data.productId); } }); // ---- policy wording links (policyWordingUrl) ---- // 1) the link inside the consent popup: text = wordingName document.querySelectorAll('[policywordingurl]').forEach(el => { el.textContent = data.wordingName || ''; el.setAttribute('href', data.policyWordingUrl || '#'); el.setAttribute('target', '_blank'); }); // 2) the "სადაზღვევო გამონაკლისები და სრული პირობები" link at the bottom // of the exclusions block document.querySelectorAll('[policywordingurlparameter]').forEach(el => { el.setAttribute('href', data.policyWordingUrl || '#'); el.setAttribute('target', '_blank'); }); // ---- buy buttons: disabled if the first payment already went through ---- const buyButtons = document.querySelectorAll('[data-gdpr-check]'); buyButtons.forEach(btn => { btn.addEventListener('click', e => { e.preventDefault(); handleBuyClick(data); }); if (data.firstPaymentIsValid === true) { setButtonDisabled(btn); } }); // ---- sticky buy popup on scroll: not needed at all if firstPaymentIsValid=true // (per spec: "we don't need this pop-up page" when the first payment already // went through) — don't even attach the scroll observer, and force it hidden // in case something else (e.g. a leftover .open class) would otherwise show it ---- if (data.firstPaymentIsValid !== true) { initStickyBuyPopup(); } else { hideStickyBuyPopup(); } // ---- keep the data around for sendsmsmail / redirect closures ---- // (a read-only copy in a global for debugging convenience) window.__policyData = data; } // ------------------------------------------------------------------ // "პოლისის შეძენა" click (both the main button and the sticky-popup button) // ------------------------------------------------------------------ function handleBuyClick(data) { if (data.firstPaymentIsValid === true) return; // just in case — the button is disabled anyway if (data.isValid === true) { redirectToPay(data.policyNumber); return; } // isValid !== true -> show the terms-consent popup first openTermsPopup(); } function redirectToPay(policyNumber) { window.location.href = PAY_URL + policyNumber; } // Disabled look for "პოლისის შეძენა" when firstPaymentIsValid === true. // Styling itself lives in Webflow (class "is-disable-btn") — this just // toggles the class and blocks interaction/a11y. function setButtonDisabled(btn) { btn.classList.add('is-disable-btn'); btn.style.pointerEvents = 'none'; btn.setAttribute('aria-disabled', 'true'); } // ------------------------------------------------------------------ // "Terms consent" popup (.c-popups-holder / .c-policy-popup) // ------------------------------------------------------------------ function initModals() { const holder = document.querySelector('.c-popups-holder'); const closeBtn = document.querySelector('.c-policy-popup__close'); const bg = document.querySelector('.c-popups-holder__bg'); const checkbox = document.querySelector('.c-policy-popup__chb input[type="checkbox"]'); // IMPORTANT: the class "c-policy-popup__chb__label" sits on TWO different // elements — an empty inside the checkbox // itself (Webflow's default label) and a
with the actual text+link // next to it. Grab the div specifically, otherwise we'd color the wrong element. const chbLabelText = document.querySelector('div.c-policy-popup__chb__label'); const submitBtn = document.querySelector('.c-policy-popup__cta[policynumberlinkaccepted]'); if (!holder) return; if (closeBtn) closeBtn.addEventListener('click', closeTermsPopup); if (bg) bg.addEventListener('click', closeTermsPopup); // clear the error (red text) as soon as the user checks the box if (checkbox && chbLabelText) { checkbox.addEventListener('change', () => { if (checkbox.checked) resetCheckboxError(chbLabelText); }); } if (submitBtn) { submitBtn.addEventListener('click', async e => { e.preventDefault(); if (!checkbox || !checkbox.checked) { showCheckboxError(chbLabelText); return; } const data = window.__policyData || {}; submitBtn.style.pointerEvents = 'none'; submitBtn.style.opacity = '0.6'; setSmsLoading(true); try { const resp = await fetch(SMS_URL, { method: 'POST', headers: { Accept: 'application/json', 'Content-Type': 'application/json' }, body: JSON.stringify(REQ_PARAMS), }); // CONFIRMED live: `description` is a free-text message, NOT a fixed // "Ok" marker — e.g. it came back as "policy is approved" for a real // approved policy (status 200, status:0, isSuccess:true), which the // old `description === 'Ok'` check treated as a FAILURE and wrongly // showed "not found" instead of redirecting to pay. The real, // reliable success flag is `isSuccess` — same field/pattern as // GetPolicyInfo. The endpoint can also reply HTTP 200 with a // completely empty body (content-length: 0, no JSON at all) — for // that case there's nothing to read `isSuccess` from, so fall back // to the HTTP status as the only signal available. const text = await resp.text(); let respData = null; if (text) { try { respData = JSON.parse(text); } catch (e) { respData = null; } } console.log('[medical-page] sendsmsmail:', resp.status, respData); const success = respData ? respData.isSuccess === true : resp.ok; if (success) { redirectToPay(data.policyNumber); } else { // there's no dedicated state for this error in the design — reuse // the general "not found" screen so the user isn't left without feedback closeTermsPopup(); showNotFound(); } } catch (err) { console.warn('[medical-page] sendsmsmail error:', err); closeTermsPopup(); showNotFound(); } finally { submitBtn.style.pointerEvents = ''; submitBtn.style.opacity = ''; setSmsLoading(false); } }); } } function openTermsPopup() { const holder = document.querySelector('.c-popups-holder'); if (holder) holder.classList.add('edit-mode'); } function closeTermsPopup() { const holder = document.querySelector('.c-popups-holder'); if (holder) holder.classList.remove('edit-mode'); } function showCheckboxError(labelTextEl) { if (!labelTextEl) return; labelTextEl.style.color = '#e5484d'; // TODO: swap for a brand red token if one exists const link = labelTextEl.querySelector('[policywordingurl]'); if (link) link.style.color = '#6853f7'; // the MED-001/26 link should turn red too (see the example in the spec doc) } function resetCheckboxError(labelTextEl) { if (!labelTextEl) return; labelTextEl.style.color = ''; const link = labelTextEl.querySelector('[policywordingurl]'); if (link) link.style.color = ''; } // Loader shown only while the sendsmsmail request (triggered by "შეიძინე") // is in flight — hidden by default, shown right before the fetch, hidden // again once it settles either way (success or error). function setSmsLoading(show) { const el = document.querySelector('.loading-animation-black_component'); if (!el) return; el.style.display = show ? 'flex' : 'none'; } // ------------------------------------------------------------------ // Sticky buy popup on scroll (#show-popup -> .modal_component-buy-policy) // ------------------------------------------------------------------ function initStickyBuyPopup() { const section = document.getElementById('show-popup'); const modal = document.querySelector('.modal_component-buy-policy'); if (!section || !modal) return; const observer = new IntersectionObserver( entries => { entries.forEach(entry => { if (entry.isIntersecting) { modal.classList.remove('open'); // scrolled back up to the section } else if (entry.boundingClientRect.top < 0) { modal.classList.add('open'); // scrolled the section past, going down } else { modal.classList.remove('open'); // haven't reached the section yet } }); }, { threshold: 0 } ); observer.observe(section); } // Force the sticky popup fully hidden when firstPaymentIsValid === true // (no scroll observer is attached in this case, so .open never gets added — // this just makes that explicit/defensive rather than relying on omission) function hideStickyBuyPopup() { const modal = document.querySelector('.modal_component-buy-policy'); if (!modal) return; modal.classList.remove('open'); modal.style.setProperty('display', 'none', 'important'); } // ------------------------------------------------------------------ // "გადმოწერე აპლიკაცია" popup (app QR code) // ------------------------------------------------------------------ function initAppModal() { const modal = document.querySelector('.modal_component'); if (!modal) return; const trigger = Array.from(document.querySelectorAll('a')).find( el => (el.textContent || '').trim() === 'გადმოწერე აპლიკაცია' ); const closeBtn = modal.querySelector('.modal_close-button'); if (trigger) { trigger.addEventListener('click', e => { e.preventDefault(); modal.classList.add('open'); }); } if (closeBtn) { closeBtn.addEventListener('click', e => { e.preventDefault(); modal.classList.remove('open'); }); } // clicking the modal's dark backdrop (the .modal_component itself) also closes it modal.addEventListener('click', e => { if (e.target === modal) modal.classList.remove('open'); }); } });