/* ════════════════════════════════════════════════════════════════ ADA Private Dental Registration Script ada-private-dental-reg-script.js Architecture mirrors AGPAL registration exactly: 1. Same waitForToken / dataversePost / fetchLeadRef / spinner pattern 2. On "Proceed to Pay": create Lead → Accounts (additional + outreach) → Contact → PaymentEvidence → redirect to /agpal-stripe-checkout 3. Stripe return handled by the shared /agpal-stripe-return page — no changes needed there. Dataverse entities: lead — main registration record accounts — additional practice + outreach sites (subgrids on Lead) contacts — accreditation contact linked to Lead ongc_paymentevidences — payment record linked to Lead ════════════════════════════════════════════════════════════════ */ (function () { 'use strict'; var ADA = { /* ── State ────────────────────────────────────────────────── */ state: { currentPage: 1, additionalSiteCount: 0, outreachSiteCount: 0, pageStructure: [1, 2, 'review'], ORDINALS: ['FIRST', 'SECOND', 'THIRD', 'FOURTH', 'FIFTH', 'SIXTH'], isBusy: false, leadId: null, leadRef: null, paymentEvidenceId: null, sigCtx: null, sigDrawing: false, /* sessionStorage keys — must not clash with AGPAL keys */ leadIdKey: 'ada_registration_lead_id', leadRefKey: 'ada_registration_lead_ref', evidenceIdKey: 'ada_registration_evidence_id', paymentInProgressKey: 'ada_payment_in_progress' }, /* ── Config (values from portalContext / Content Snippets) ── */ config: { baseFee: parseFloat((window.portalContext || {}).baseFee) || 2200, additionalSiteFee: parseFloat((window.portalContext || {}).additionalSiteFee) || 550, outreachFee: parseFloat((window.portalContext || {}).outreachFee) || 110, additionalDiscount: 0.15, ccSurchargeRate: parseFloat((window.portalContext || {}).ccSurchargeRate) || 0.01927, gstRate: parseFloat((window.portalContext || {}).gstRate) || 0.10, contactTypeId: (window.portalContext || {}).contactTypeId || '', accountLeadLookupField: 'originatingleadid@odata.bind' }, /* ── Init ─────────────────────────────────────────────────── */ init: function () { this.injectSpinnerOverlay(); this.clearSessionData(); this.buildProgress(); this.calculateFee(); console.log('[ADA] App initialised'); }, /* ════════════════════════════════════════════════════════════ SPINNER ════════════════════════════════════════════════════════════ */ injectSpinnerOverlay: function () { if (document.getElementById('ada-spinner-overlay')) return; var style = document.createElement('style'); style.textContent = '@keyframes ada-spin{to{transform:rotate(360deg)}}' + '#ada-spinner-overlay{display:none;position:fixed;inset:0;background:rgba(0,62,82,0.65);' + 'z-index:99999;align-items:center;justify-content:center;flex-direction:column;gap:18px;}'; document.head.appendChild(style); var overlay = document.createElement('div'); overlay.id = 'ada-spinner-overlay'; overlay.setAttribute('aria-live', 'assertive'); overlay.innerHTML = '
' + '
Please wait\u2026
'; document.body.appendChild(overlay); }, setBusy: function (busy, message) { this.state.isBusy = busy; var overlay = document.getElementById('ada-spinner-overlay'); var msgEl = document.getElementById('ada-spinner-msg'); if (overlay) overlay.style.display = busy ? 'flex' : 'none'; if (msgEl && message) msgEl.textContent = message; if (msgEl && !busy) msgEl.textContent = 'Please wait\u2026'; /* Disable/restore all buttons */ document.querySelectorAll('button').forEach(function (btn) { if (busy) { btn.setAttribute('data-was-disabled', btn.disabled ? '1' : '0'); btn.disabled = true; } else { btn.disabled = btn.getAttribute('data-was-disabled') === '1'; } }); }, /* ════════════════════════════════════════════════════════════ CSRF TOKEN ════════════════════════════════════════════════════════════ */ waitForToken: function () { return new Promise(function (resolve, reject) { /* Fast path — already in portalContext */ var pc = (window.portalContext || {}); var cached = pc.csrfToken || pc.requestVerificationToken; if (cached && cached.length > 20) { resolve(cached); return; } /* Hidden input from portal layout */ var domEl = document.querySelector('input[name="__RequestVerificationToken"]'); if (domEl && domEl.value) { resolve(domEl.value); return; } /* Inline script hidden input */ var csrfEl = document.getElementById('csrf-token-value'); if (csrfEl && csrfEl.value && csrfEl.value.length > 20) { resolve(csrfEl.value); return; } /* Poll briefly */ var attempts = 0; var poll = setInterval(function () { attempts++; var pv = (window.portalContext || {}).csrfToken; if (pv && pv.length > 20) { clearInterval(poll); resolve(pv); return; } var el = document.getElementById('csrf-token-value'); if (el && el.value && el.value.length > 20) { clearInterval(poll); resolve(el.value); return; } if (attempts >= 30) { clearInterval(poll); fetch('/Account/Login', { credentials: 'same-origin', headers: { 'Accept': 'text/html' } }) .then(function (r) { return r.text(); }) .then(function (html) { var m = html.match(/name="__RequestVerificationToken"[^>]*value="([^"]+)"/); if (!m) m = html.match(/value="([^"]{40,})"[^>]*name="__RequestVerificationToken"/); if (m && m[1]) { resolve(m[1]); } else { reject(new Error('CSRF token not available')); } }) .catch(function (e) { reject(new Error('CSRF token fetch failed: ' + e.message)); }); } }, 150); }); }, /* ════════════════════════════════════════════════════════════ DATAVERSE API ════════════════════════════════════════════════════════════ */ extractGuidFromOdata: function (data) { var id = (data && data['@odata.id']) || ''; var m = id.match(/\(([0-9a-f-]{36})\)/i); return m ? m[1] : null; }, extractEntityId: function (data, xhrOrResponse) { if (data && typeof data === 'object') { var fromBody = data.leadid || data.contactid || data.accountid || data.ongc_paymentevidenceid || this.extractGuidFromOdata(data); if (fromBody) return fromBody; } var headerVal = ''; if (xhrOrResponse) { if (typeof xhrOrResponse.headers === 'object' && typeof xhrOrResponse.headers.get === 'function') { headerVal = xhrOrResponse.headers.get('OData-EntityId') || ''; } if (!headerVal && typeof xhrOrResponse.getResponseHeader === 'function') { headerVal = xhrOrResponse.getResponseHeader('OData-EntityId') || ''; } } if (headerVal) { var m2 = headerVal.match(/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/i); if (m2) return m2[0]; } return null; }, parseApiError: function (xhrOrResponse) { try { if (xhrOrResponse && xhrOrResponse.responseJSON && xhrOrResponse.responseJSON.error) return xhrOrResponse.responseJSON.error.message || 'Unknown error'; } catch (e) { } try { if (xhrOrResponse && xhrOrResponse.responseText) { var parsed = JSON.parse(xhrOrResponse.responseText); return (parsed && parsed.error && parsed.error.message) || 'Unknown error'; } } catch (e) { } return 'Unknown error'; }, dataversePost: async function (entitySetName, payload) { var url = '/_api/' + entitySetName; if (window.webapi && typeof window.webapi.safeAjax === 'function') { var self = this; return new Promise(function (resolve, reject) { window.webapi.safeAjax({ type: 'POST', url: url, contentType: 'application/json', data: JSON.stringify(payload), success: function (data, status, xhr) { resolve({ id: self.extractEntityId(data, xhr), data: data || {} }); }, error: function (xhr) { reject(new Error(self.parseApiError(xhr))); } }); }); } var token = await this.waitForToken(); var response = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json', '__RequestVerificationToken': token, 'OData-MaxVersion': '4.0', 'OData-Version': '4.0', 'Prefer': 'return=representation' }, body: JSON.stringify(payload) }); if (!response.ok) { var errMsg = 'API request failed'; try { var errJson = await response.json(); errMsg = (errJson && errJson.error && errJson.error.message) || errMsg; } catch (e) { } throw new Error(errMsg); } var data = {}; try { data = await response.json(); } catch (e) { } return { id: this.extractEntityId(data, response), data: data }; }, /* ── Fetch autonumber leadRef after lead creation ── */ fetchLeadRef: async function (leadId) { try { var token = await this.waitForToken(); var r = await fetch( '/_api/leads(' + leadId + ')?$select=ongc_leadrefid,ongc_clientid', { method: 'GET', headers: { '__RequestVerificationToken': token, 'OData-MaxVersion': '4.0', 'OData-Version': '4.0', 'Accept': 'application/json' } } ); if (!r.ok) { console.warn('[ADA] fetchLeadRef HTTP ' + r.status); return null; } var d = await r.json(); var ref = (d && (d.ongc_leadrefid || d.ongc_clientid)) || null; console.log('[ADA] fetchLeadRef:', ref); return ref; } catch (e) { console.warn('[ADA] fetchLeadRef error:', e.message); return null; } }, /* ════════════════════════════════════════════════════════════ SESSION STORAGE ════════════════════════════════════════════════════════════ */ clearSessionData: function () { var self = this; [this.state.leadIdKey, this.state.leadRefKey, this.state.evidenceIdKey, this.state.paymentInProgressKey].forEach(function (k) { try { sessionStorage.removeItem(k); } catch (e) { } }); this.state.leadId = null; this.state.leadRef = null; this.state.paymentEvidenceId = null; }, saveSession: function () { try { if (this.state.leadId) sessionStorage.setItem(this.state.leadIdKey, this.state.leadId); if (this.state.leadRef) sessionStorage.setItem(this.state.leadRefKey, this.state.leadRef); if (this.state.paymentEvidenceId) sessionStorage.setItem(this.state.evidenceIdKey, this.state.paymentEvidenceId); } catch (e) { } }, /* ════════════════════════════════════════════════════════════ HELPERS ════════════════════════════════════════════════════════════ */ round2: function (n) { return Math.round(n * 100) / 100; }, gVal: function (id) { var el = document.getElementById(id); return el ? el.value.trim() : ''; }, setRv: function (id, val) { var el = document.getElementById(id); if (el) el.textContent = val; }, stripEmpty: function (payload) { Object.keys(payload).forEach(function (k) { if (payload[k] === '' || payload[k] === null || payload[k] === undefined) delete payload[k]; }); return payload; }, /* ════════════════════════════════════════════════════════════ FEE CALCULATION ════════════════════════════════════════════════════════════ */ calcFees: function () { var cfg = this.config; var s = this.state; var disc = s.additionalSiteCount > 1 ? cfg.additionalDiscount : 0; var base = cfg.baseFee + (s.additionalSiteCount * cfg.additionalSiteFee * (1 - disc)) + (s.outreachSiteCount * cfg.outreachFee); var surcharge = this.round2(base * cfg.ccSurchargeRate); var gst = this.round2((base + surcharge) * cfg.gstRate); var total = this.round2(base + surcharge + gst); return { base: this.round2(base), surcharge: surcharge, gst: gst, total: total }; }, calculateFee: function () { var fees = this.calcFees(); var fmt = function (n) { return n.toLocaleString('en-AU', { minimumFractionDigits: 2, maximumFractionDigits: 2 }); }; var total = fees.total; ['reviewFeeAmount', 'payBtnAmount'].forEach(function (id) { var el = document.getElementById(id); if (el) el.textContent = fmt(total); }); var s = this.state; var breakdown = '1 main site'; if (s.additionalSiteCount > 0) breakdown += ' + ' + s.additionalSiteCount + ' additional site' + (s.additionalSiteCount > 1 ? 's' : ''); if (s.outreachSiteCount > 0) breakdown += ' + ' + s.outreachSiteCount + ' outreach'; var fbEl = document.getElementById('feeBreakdown'); if (fbEl) fbEl.textContent = breakdown; var atMap = { desktop: 'Desktop Assessment', onsite: 'On-site Assessment' }; var atEl = document.getElementById('assessmentType'); var atlEl = document.getElementById('assessTypeLabel'); if (atEl && atlEl) atlEl.textContent = atMap[atEl.value] || ''; }, /* ════════════════════════════════════════════════════════════ PROGRESS BAR ════════════════════════════════════════════════════════════ */ buildProgress: function () { var c = document.getElementById('progressSteps'); if (!c) return; c.innerHTML = ''; var self = this; this.state.pageStructure.forEach(function (pg, idx) { var pNum = idx + 1; var isActive = self.state.currentPage === pNum; var isDone = self.state.currentPage > pNum; var label = pg === 1 ? 'Practice' : pg === 2 ? 'Assessment' : pg === 'review' ? 'Submit' : (typeof pg === 'string' && pg.indexOf('add') === 0) ? 'Site ' + pg.slice(3) : (typeof pg === 'string' && pg.indexOf('out') === 0) ? 'Out.' + pg.slice(3) : ''; var div = document.createElement('div'); div.className = 'ada-step-item' + (isActive ? ' active' : '') + (isDone ? ' done' : ''); div.innerHTML = '
' + (isDone ? '✓' : pNum) + '
' + '
' + label + '
'; c.appendChild(div); }); var counter = document.getElementById('pageCounter'); if (counter) counter.textContent = 'Page ' + this.state.currentPage + ' of ' + this.state.pageStructure.length; }, /* ════════════════════════════════════════════════════════════ PAGE NAVIGATION ════════════════════════════════════════════════════════════ */ showPage: function (pNum) { if (this.state.isBusy) return; this.state.currentPage = pNum; var pg = this.state.pageStructure[pNum - 1]; document.querySelectorAll('.ada-form-page').forEach(function (el) { el.classList.remove('active'); }); var target; if (pg === 'review') { target = document.getElementById('reviewPage'); } else if (typeof pg === 'string') { target = document.querySelector('.ada-form-page[data-dynpage="' + pg + '"]'); } else { target = document.querySelector('.ada-form-page[data-page="' + pg + '"]'); } if (target) { target.classList.add('active'); window.scrollTo({ top: 0, behavior: 'smooth' }); } this.buildProgress(); }, goToPage: function (n) { this.showPage(n); }, assessmentNextClick: function () { var pgIdx = this.state.pageStructure.indexOf(2); var nextIdx = pgIdx + 1; if (this.state.pageStructure[nextIdx] === 'review') this.populateReview(); this.showPage(nextIdx + 1); }, reviewBack: function () { var revIdx = this.state.pageStructure.indexOf('review'); this.showPage(revIdx); }, dynNavNext: function (key) { var idx = this.state.pageStructure.indexOf(key); if (this.state.pageStructure[idx + 1] === 'review') this.populateReview(); this.showPage(idx + 2); }, dynNavPrev: function (key) { var idx = this.state.pageStructure.indexOf(key); this.showPage(idx); }, rebuildPageStructure: function () { var s = [1, 2]; for (var i = 1; i <= this.state.additionalSiteCount; i++) s.push('add' + i); for (var j = 1; j <= this.state.outreachSiteCount; j++) s.push('out' + j); s.push('review'); this.state.pageStructure = s; this.buildProgress(); this.calculateFee(); }, /* ════════════════════════════════════════════════════════════ DYNAMIC SITE PAGES ════════════════════════════════════════════════════════════ */ buildSitePage: function (key, title) { if (document.querySelector('.ada-form-page[data-dynpage="' + key + '"]')) return; var isOut = key.indexOf('out') === 0; var num = parseInt(key.slice(3)); var pfx = isOut ? 'Outreach Site ' + num : 'Site ' + (num + 1); var stateOpts = '' + '' + '' + '' + ''; var prefixOpts = '' + '' + ''; var el = document.createElement('div'); el.className = 'ada-form-page'; el.setAttribute('data-dynpage', key); var self = this; el.innerHTML = '
' + '
' + title + '
' + '
' + '
' + '
' + '
' + '
' + '
' + '
' + '
' + '
Prefix
' + '
First Name
' + '
Last Name
' + '
' + '
' + '
' + '
' + '
' + '
' + '
Prefix
' + '
First
' + '
Last
' + '
' + '
' + '
' + '
' + '
' + '
' + '
' + '' + '
Address Line 1
' + '
' + '
City
' + '
State
' + '
Postcode
' + '
' + '
' + '
' + '' + '
' + '' + '
'; var reviewPage = document.getElementById('reviewPage'); document.getElementById('formShell').insertBefore(el, reviewPage); }, removeDynPages: function (prefix) { document.querySelectorAll('.ada-form-page[data-dynpage^="' + prefix + '"]').forEach(function (el) { el.remove(); }); }, buildAllAdditionalPages: function () { this.removeDynPages('add'); for (var i = 1; i <= this.state.additionalSiteCount; i++) { var ord = this.state.ORDINALS[i - 1] || (i + 'TH'); this.buildSitePage('add' + i, ord + ' ADDITIONAL SITE (SITE ' + (i + 1) + ')'); } }, buildAllOutreachPages: function () { this.removeDynPages('out'); for (var i = 1; i <= this.state.outreachSiteCount; i++) { this.buildSitePage('out' + i, 'OUTREACH SITE ' + i); } }, /* ════════════════════════════════════════════════════════════ ASSESSMENT PAGE HANDLERS ════════════════════════════════════════════════════════════ */ handleAdditionalChange: function () { var v = this.gVal('hasAdditional'); document.getElementById('additionalCountWrap').style.display = (v === 'yes') ? 'block' : 'none'; if (v !== 'yes') { this.state.additionalSiteCount = 0; this.removeDynPages('add'); this.rebuildPageStructure(); } }, handleOutreachChange: function () { var v = this.gVal('hasOutreach'); document.getElementById('outreachCountWrap').style.display = (v === 'yes') ? 'block' : 'none'; if (v !== 'yes') { this.state.outreachSiteCount = 0; this.removeDynPages('out'); this.rebuildPageStructure(); } }, onAdditionalCountChange: function () { this.state.additionalSiteCount = parseInt(this.gVal('additionalCount')) || 0; this.buildAllAdditionalPages(); this.rebuildPageStructure(); }, onOutreachCountChange: function () { this.state.outreachSiteCount = parseInt(this.gVal('outreachCount')) || 0; this.buildAllOutreachPages(); this.rebuildPageStructure(); }, /* ════════════════════════════════════════════════════════════ CONDITIONAL FIELD TOGGLES ════════════════════════════════════════════════════════════ */ toggleAdaMember: function () { var v = this.gVal('adaMember'); document.getElementById('adaMemberNumField').style.display = (v === 'yes') ? 'block' : 'none'; document.getElementById('nonAdaNameBlock').style.display = (v === 'no') ? 'block' : 'none'; }, toggleCorporate: function () { document.getElementById('corporateNameField').style.display = (this.gVal('corporateGroup') === 'yes') ? 'block' : 'none'; }, togglePostalAddress: function () { var checked = document.querySelector('input[name="postalDiff"]:checked'); document.getElementById('postalAddressBlock').style.display = (checked && checked.value === 'yes') ? 'block' : 'none'; }, /* ════════════════════════════════════════════════════════════ REVIEW PAGE ════════════════════════════════════════════════════════════ */ populateReview: function () { var self = this; this.setRv('rv-practiceName', this.gVal('practiceName')); this.setRv('rv-tradingName', this.gVal('tradingName')); this.setRv('rv-abn', this.gVal('practiceABN')); var adaVal = this.gVal('adaMember'); var ownerStr = adaVal === 'no' ? [this.gVal('nonAdaPrefix'), this.gVal('nonAdaFirst'), this.gVal('nonAdaLast')].filter(Boolean).join(' ') : [this.gVal('ownerPrefix'), this.gVal('ownerFirst'), this.gVal('ownerLast')].filter(Boolean).join(' '); this.setRv('rv-owner', ownerStr || '\u2014'); this.setRv('rv-adaMember', adaVal === 'yes' ? 'Yes \u2014 ' + this.gVal('adaMemberNum') : adaVal === 'no' ? 'No' : '\u2014'); var corp = this.gVal('corporateGroup'); this.setRv('rv-corporate', corp === 'yes' ? 'Yes \u2014 ' + this.gVal('corporateName') : corp === 'no' ? 'No' : '\u2014'); var addr = [this.gVal('addrLine0'), this.gVal('addrCity'), this.gVal('addrState'), this.gVal('addrPostcode')].filter(Boolean).join(', '); this.setRv('rv-address', addr || '\u2014'); var postalChk = document.querySelector('input[name="postalDiff"]:checked'); if (postalChk && postalChk.value === 'yes') { var pa = [this.gVal('postalLine0'), this.gVal('postalCity'), this.gVal('postalState'), this.gVal('postalPostcode')].filter(Boolean).join(', '); this.setRv('rv-postal', pa || '\u2014'); } else { this.setRv('rv-postal', 'Same as street address'); } this.setRv('rv-phone', this.gVal('practicePhone')); this.setRv('rv-email', this.gVal('practiceEmail')); var atMap = { desktop: 'Desktop Assessment', onsite: 'On-site Assessment' }; this.setRv('rv-assessType', atMap[this.gVal('assessmentType')] || this.gVal('assessmentType')); var ha = this.gVal('hasAdditional'); this.setRv('rv-additional', ha === 'yes' ? 'Yes \u2014 ' + this.gVal('additionalCount') + ' site(s)' : ha === 'no' ? 'No' : '\u2014'); var ho = this.gVal('hasOutreach'); this.setRv('rv-outreach', ho === 'yes' ? 'Yes \u2014 ' + this.gVal('outreachCount') + ' location(s)' : ho === 'no' ? 'No' : '\u2014'); /* Additional sites summary */ var sitesWrap = document.getElementById('rv-sites-wrap'); var sitesCont = document.getElementById('rv-sites-content'); if (this.state.additionalSiteCount > 0 && sitesWrap && sitesCont) { sitesWrap.style.display = 'block'; var sh = ''; for (var i = 1; i <= this.state.additionalSiteCount; i++) { var k = 'add' + i; sh += '
' + 'Site ' + (i + 1) + '' + '
Practice Name' + (this.gVal(k + '-name') || '\u2014') + '
' + '
City / State' + (this.gVal(k + '-city') || '\u2014') + ', ' + (this.gVal(k + '-state') || '\u2014') + '
' + '
Contact Phone' + (this.gVal(k + '-phone') || '\u2014') + '
' + '
'; } sitesCont.innerHTML = sh; } else if (sitesWrap) { sitesWrap.style.display = 'none'; } /* Outreach sites summary */ var outWrap = document.getElementById('rv-outreach-wrap'); var outCont = document.getElementById('rv-outreach-content'); if (this.state.outreachSiteCount > 0 && outWrap && outCont) { outWrap.style.display = 'block'; var oh = ''; for (var j = 1; j <= this.state.outreachSiteCount; j++) { var ok = 'out' + j; oh += '
' + 'Outreach ' + j + '' + '
Practice Name' + (this.gVal(ok + '-name') || '\u2014') + '
' + '
City / State' + (this.gVal(ok + '-city') || '\u2014') + ', ' + (this.gVal(ok + '-state') || '\u2014') + '
' + '
Contact Phone' + (this.gVal(ok + '-phone') || '\u2014') + '
' + '
'; } outCont.innerHTML = oh; } else if (outWrap) { outWrap.style.display = 'none'; } /* Pre-fill authorised rep from contact */ var cf = document.getElementById('contactFirst'); var cl = document.getElementById('contactLast'); var af = document.getElementById('authFirst'); var al = document.getElementById('authLast'); if (cf && cf.value && af && !af.value) af.value = cf.value; if (cl && cl.value && al && !al.value) al.value = cl.value; this.calculateFee(); this.initSig(); }, /* ════════════════════════════════════════════════════════════ SIGNATURE ════════════════════════════════════════════════════════════ */ initSig: function () { var c = document.getElementById('sig-canvas'); if (!c) return; c.width = c.parentElement.clientWidth || 680; c.height = 110; /* Clone to remove stale listeners */ var fresh = c.cloneNode(true); c.parentNode.replaceChild(fresh, c); c = fresh; this.state.sigCtx = c.getContext('2d'); this.state.sigCtx.strokeStyle = '#003E52'; this.state.sigCtx.lineWidth = 2.2; this.state.sigCtx.lineCap = 'round'; this.state.sigCtx.lineJoin = 'round'; var ctx = this.state.sigCtx; var self = this; function pos(e) { var r = c.getBoundingClientRect(); var pt = e.touches ? e.touches[0] : e; return { x: pt.clientX - r.left, y: pt.clientY - r.top }; } c.addEventListener('mousedown', function (e) { self.state.sigDrawing = true; ctx.beginPath(); var p = pos(e); ctx.moveTo(p.x, p.y); }); c.addEventListener('mousemove', function (e) { if (!self.state.sigDrawing) return; var p = pos(e); ctx.lineTo(p.x, p.y); ctx.stroke(); }); c.addEventListener('mouseup', function () { self.state.sigDrawing = false; }); c.addEventListener('mouseleave', function () { self.state.sigDrawing = false; }); c.addEventListener('touchstart', function (e) { e.preventDefault(); self.state.sigDrawing = true; ctx.beginPath(); var p = pos(e); ctx.moveTo(p.x, p.y); }, { passive: false }); c.addEventListener('touchmove', function (e) { e.preventDefault(); if (!self.state.sigDrawing) return; var p = pos(e); ctx.lineTo(p.x, p.y); ctx.stroke(); }, { passive: false }); c.addEventListener('touchend', function () { self.state.sigDrawing = false; }); }, clearSig: function () { var c = document.getElementById('sig-canvas'); if (this.state.sigCtx && c) this.state.sigCtx.clearRect(0, 0, c.width, c.height); }, isSigEmpty: function () { var c = document.getElementById('sig-canvas'); if (!c || !this.state.sigCtx) return true; var d = this.state.sigCtx.getImageData(0, 0, c.width, c.height).data; for (var i = 3; i < d.length; i += 4) { if (d[i] > 0) return false; } return true; }, /* ════════════════════════════════════════════════════════════ DATAVERSE PAYLOAD BUILDERS ════════════════════════════════════════════════════════════ */ buildLeadPayload: function () { var g = this.gVal.bind(this); var practiceName = g('practiceName'); var postalChk = document.querySelector('input[name="postalDiff"]:checked'); var differentPostal = postalChk && postalChk.value === 'yes'; var corporateGroup = g('corporateGroup'); var payload = { /* Lead subject uses the page title as requested */ subject: 'ADA Private Dental Registration \u2013 ' + practiceName, /* Accreditation contact is the Lead owner */ firstname: g('contactFirst'), lastname: g('contactLast') || practiceName, jobtitle: g('contactPosition'), emailaddress1: g('contactEmail') || g('practiceEmail'), telephone1: g('contactPhone'), mobilephone: g('practicePhone'), fax: g('practiceFax'), companyname: practiceName, websiteurl: g('practiceWeb'), leadsourcecode: 8, ongc_assessmenttype: 107150000, /* Street address */ address1_line1: g('addrLine0'), address1_line2: g('addrLine1'), address1_city: g('addrCity'), address1_stateorprovince: g('addrState'), address1_postalcode: g('addrPostcode'), address1_country: 'Australia', /* Postal address */ address2_line1: differentPostal ? g('postalLine0') : '', address2_line2: differentPostal ? g('postalLine1') : '', address2_city: differentPostal ? g('postalCity') : '', address2_stateorprovince: differentPostal ? g('postalState') : '', address2_postalcode: differentPostal ? g('postalPostcode') : '', address2_country: differentPostal ? 'Australia' : '', /* Custom ONGC fields shared with AGPAL */ ongc_enquirytype: 1, ongc_practicename: practiceName, ongc_practiceabn: g('practiceABN'), ongc_practicephone: g('practicePhone'), ongc_postalsameasstreet: !differentPostal, ongc_corporateentity: corporateGroup === 'yes' ? g('corporateName') : '', ongc_groupname: corporateGroup === 'yes' ? g('corporateName') : '' }; return this.stripEmpty(payload); }, buildContactPayload: function () { var g = this.gVal.bind(this); var payload = { firstname: g('contactFirst'), lastname: g('contactLast'), jobtitle: g('contactPosition'), emailaddress1: g('contactEmail'), telephone1: g('contactPhone'), mobilephone: g('practicePhone') }; if (this.state.leadId) { payload['originatingleadid@odata.bind'] = '/leads(' + this.state.leadId + ')'; } if (this.config.contactTypeId) { payload['ongc_ContactType@odata.bind'] = '/ongc_contacttypes(' + this.config.contactTypeId + ')'; } return this.stripEmpty(payload); }, buildAccountPayload: function (key, accountType) { /* accountType: 'Additional Practice' | 'Outreach Location' These accounts appear in the two subgrids on the Lead record. See ADA.config.accountLeadLookupField for the relationship. */ var g = this.gVal.bind(this); var payload = { name: g(key + '-name'), description: accountType, address1_line1: g(key + '-addr'), address1_city: g(key + '-city'), address1_stateorprovince: g(key + '-state'), address1_postalcode: g(key + '-postcode'), address1_country: 'Australia', telephone1: g(key + '-phone'), emailaddress1: g(key + '-email') }; /* Link Account to Lead so it appears in the subgrid. Field name to be confirmed with Dataverse team — see config.accountLeadLookupField */ //if (this.state.leadId && this.config.accountLeadLookupField) { // payload[this.config.accountLeadLookupField] = '/leads(' + this.state.leadId + ')'; //} return this.stripEmpty(payload); }, buildPaymentEvidencePayload: function () { var fees = this.calcFees(); var g = this.gVal.bind(this); var payload = { ongc_payername: (g('contactFirst') + ' ' + g('contactLast')).trim(), ongc_paymentamount: fees.base, ongc_surchargefee: fees.surcharge, ongc_gst: fees.gst, ongc_totalamount: fees.total, ongc_paymentstatus: 'pending-cc', ongc_transactionstatus: 'pending', ongc_paymentdate: new Date().toISOString(), ongc_responsemessage: 'Pending \u2013 ADA Private Dental Stripe credit-card checkout initiated' }; if (this.state.leadId) { payload['ongc_Lead@odata.bind'] = '/leads(' + this.state.leadId + ')'; } return this.stripEmpty(payload); }, /* ════════════════════════════════════════════════════════════ RECORD CREATION CHAIN ════════════════════════════════════════════════════════════ */ createAllRecords: async function () { /* ── 1. Lead ── */ this.setBusy(true, 'Creating registration record\u2026'); var lead = await this.dataversePost('leads', this.buildLeadPayload()); this.state.leadId = lead.id; if (!this.state.leadId) throw new Error('Lead created but GUID not returned. Check Dataverse API permissions.'); console.log('[ADA] Lead created:', this.state.leadId); var baseUrl = window.location.origin + '/_api'; ///* ── 2. Fetch autonumber reference ── */ //this.setBusy(true, 'Fetching registration reference\u2026'); //var fetchedRef = await this.fetchLeadRef(this.state.leadId); //this.state.leadRef = fetchedRef || null; //console.log('[ADA] leadRef:', this.state.leadRef); //this.saveSession(); ///* ── 3. Additional practice Accounts ── */ //if (this.state.additionalSiteCount > 0) { // this.setBusy(true, 'Creating additional practice records\u2026'); // var addPromises = []; // for (var i = 1; i <= this.state.additionalSiteCount; i++) { // addPromises.push(this.dataversePost('accounts', this.buildAccountPayload('add' + i, 'Additional Practice'))); // } // var addResults = await Promise.allSettled(addPromises); // addResults.forEach(function (r, idx) { // if (r.status === 'rejected') console.warn('[ADA] Additional account ' + (idx + 1) + ' failed:', r.reason && r.reason.message); // else console.log('[ADA] Additional account ' + (idx + 1) + ' created:', r.value && r.value.id); // }); //} ///* ── 4. Outreach location Accounts ── */ //if (this.state.outreachSiteCount > 0) { // this.setBusy(true, 'Creating outreach location records\u2026'); // var outPromises = []; // for (var j = 1; j <= this.state.outreachSiteCount; j++) { // outPromises.push(this.dataversePost('accounts', this.buildAccountPayload('out' + j, 'Outreach Location'))); // } // var outResults = await Promise.allSettled(outPromises); // outResults.forEach(function (r, idx) { // if (r.status === 'rejected') console.warn('[ADA] Outreach account ' + (idx + 1) + ' failed:', r.reason && r.reason.message); // else console.log('[ADA] Outreach account ' + (idx + 1) + ' created:', r.value && r.value.id); // }); //} ///* ── 5. Contact ── */ //this.setBusy(true, 'Creating contact record\u2026'); //var contactResult = await Promise.allSettled([this.dataversePost('contacts', this.buildContactPayload())]); //if (contactResult[0].status === 'rejected') console.warn('[ADA] Contact failed:', contactResult[0].reason && contactResult[0].reason.message); //else console.log('[ADA] Contact created:', contactResult[0].value && contactResult[0].value.id); ///* ── 6. PaymentEvidence ── */ //this.setBusy(true, 'Creating payment record\u2026'); //var evidence = await this.dataversePost('ongc_paymentevidences', this.buildPaymentEvidencePayload()); //if (evidence && evidence.id) { // this.state.paymentEvidenceId = evidence.id; // console.log('[ADA] Evidence created:', evidence.id); //} else { // console.warn('[ADA] Evidence created but GUID not returned'); //} //this.saveSession(); /* ── 2. Fetch autonumber reference ── */ this.setBusy(true, 'Fetching registration reference…'); var fetchedRef = await this.fetchLeadRef(this.state.leadId); this.state.leadRef = fetchedRef || null; console.log('[ADA] leadRef:', this.state.leadRef); this.saveSession(); /* ── 3. Additional practice Accounts ── */ if (this.state.additionalSiteCount > 0) { this.setBusy(true, 'Creating additional practice records…'); for (var i = 1; i <= this.state.additionalSiteCount; i++) { var acc = await this.dataversePost('accounts', this.buildAccountPayload('add' + i, 'Additional Practice')); console.log('[ADA] Additional account created:', acc.id); // Link account via M:M relationship await this.dataversePost('leads(' + this.state.leadId + ')/ongc_Lead_Account_AdditionalPractice/$ref', { '@odata.id': baseUrl + '/accounts(' + acc.id + ')' }); } } /* ── 4. Outreach location Accounts ── */ if (this.state.outreachSiteCount > 0) { this.setBusy(true, 'Creating outreach location records…'); for (var j = 1; j <= this.state.outreachSiteCount; j++) { var accOut = await this.dataversePost('accounts', this.buildAccountPayload('out' + j, 'Outreach Location')); console.log('[ADA] Outreach account created:', accOut.id); // Link account via M:M relationship await this.dataversePost('leads(' + this.state.leadId + ')/ongc_Lead_Account_OutreachLocation/$ref', { '@odata.id': baseUrl + '/accounts(' + accOut.id + ')' }); } } /* ── 5. Contact ── */ this.setBusy(true, 'Creating contact record…'); var contactResult = await this.dataversePost('contacts', this.buildContactPayload()); console.log('[ADA] Contact created:', contactResult.id); /* ── 6. PaymentEvidence ── */ this.setBusy(true, 'Creating payment record…'); var evidence = await this.dataversePost('ongc_paymentevidences', this.buildPaymentEvidencePayload()); this.state.paymentEvidenceId = evidence.id; console.log('[ADA] Evidence created:', evidence.id); this.saveSession(); }, /* ════════════════════════════════════════════════════════════ STRIPE REDIRECT (uses shared /agpal-stripe-checkout) ════════════════════════════════════════════════════════════ */ initiateStripePayment: function () { var fees = this.calcFees(); var g = this.gVal.bind(this); var params = new URLSearchParams({ leadId: this.state.leadId || '', leadRef: this.state.leadRef || '', paymentEvidenceId: this.state.paymentEvidenceId || '', amount: fees.total.toFixed(2), base: fees.base.toFixed(2), surcharge: fees.surcharge.toFixed(2), gst: fees.gst.toFixed(2), email: g('contactEmail') || g('practiceEmail'), name: (g('contactFirst') + ' ' + g('contactLast')).trim(), ref: 'ADA Private Dental Registration \u2013 ' + g('practiceName'), returnUrl: window.location.href.split('?')[0] }); /* Persist before redirect — /agpal-stripe-return reads these */ try { sessionStorage.setItem(this.state.paymentInProgressKey, '1'); if (this.state.leadId) sessionStorage.setItem(this.state.leadIdKey, this.state.leadId); if (this.state.leadRef) sessionStorage.setItem(this.state.leadRefKey, this.state.leadRef); if (this.state.paymentEvidenceId) sessionStorage.setItem(this.state.evidenceIdKey, this.state.paymentEvidenceId); } catch (e) { } /* Use location.replace so browser Back can't return to mid-payment form */ window.location.replace('/agpal-stripe-checkout?' + params.toString()); }, /* ════════════════════════════════════════════════════════════ MAIN PAYMENT ENTRY POINT (called by "Proceed to Pay") ════════════════════════════════════════════════════════════ */ proceedToPay: async function () { /* ── Validate declarations ── */ var auth = document.getElementById('chk-auth'); var tc = document.getElementById('chk-tc'); var confirm = document.getElementById('chk-confirm'); var af = this.gVal('authFirst'); var al = this.gVal('authLast'); var errEl = document.getElementById('submitError'); if (!auth || !auth.checked || !tc || !tc.checked || !confirm || !confirm.checked || !af || !al || this.isSigEmpty()) { if (errEl) { errEl.style.display = 'flex'; errEl.scrollIntoView({ behavior: 'smooth', block: 'center' }); } return; } if (errEl) errEl.style.display = 'none'; try { /* Create Lead + Accounts + Contact + Evidence */ await this.createAllRecords(); /* Redirect to shared Stripe checkout page */ this.setBusy(true, 'Redirecting to payment\u2026'); this.initiateStripePayment(); } catch (err) { console.error('[ADA] proceedToPay failed', err); var errMsg = document.getElementById('submitError'); if (errMsg) { errMsg.style.display = 'flex'; errMsg.textContent = 'Could not submit registration: ' + (err.message || 'Unknown error') + '. Please try again.'; errMsg.scrollIntoView({ behavior: 'smooth', block: 'center' }); } this.setBusy(false); } } }; /* end ADA object */ /* ── Window-level delegates for inline onclick handlers in HTML ── */ window.ADA = ADA; window.toggleAdaMember = function () { ADA.toggleAdaMember(); }; window.toggleCorporate = function () { ADA.toggleCorporate(); }; window.togglePostalAddress = function () { ADA.togglePostalAddress(); }; window.handleAdditionalChange = function () { ADA.handleAdditionalChange(); }; window.handleOutreachChange = function () { ADA.handleOutreachChange(); }; window.onAdditionalCountChange = function () { ADA.onAdditionalCountChange(); }; window.onOutreachCountChange = function () { ADA.onOutreachCountChange(); }; window.assessmentNextClick = function () { ADA.assessmentNextClick(); }; window.reviewBack = function () { ADA.reviewBack(); }; window.saveAndResume = function () { alert('Your progress has been saved.'); }; window.clearSig = function () { ADA.clearSig(); }; window.proceedToPay = function () { ADA.proceedToPay(); }; window.addEventListener('resize', function () { if (ADA.state.sigCtx) ADA.initSig(); }); document.addEventListener('DOMContentLoaded', function () { ADA.init(); }); }());