/* ════════════════════════════════════════════════════════════════ 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) || 1712.70, additionalSiteFee: parseFloat((window.portalContext || {}).additionalSiteFee) || 1141.80, outreachFee: parseFloat((window.portalContext || {}).outreachFee) || 114.40, additionalDiscount: 0, ccSurchargeRate: parseFloat((window.portalContext || {}).ccSurchargeRate) || 0.01927, gstRate: parseFloat((window.portalContext || {}).gstRate) || 0.10, contactTypeId: (window.portalContext || {}).contactTypeId || '', businessId: (window.portalContext || {}).businessId || '', leadRefFlowUrl: (window.portalContext || {}).leadRefFlowUrl || '', 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 ongc_leadrefid via the shared Power Automate HTTP-trigger flow. Anonymous has no Read on Lead, so the previous GET-based fetchLeadRef always 403'd with EntityPermissionReadIsMissing — confirmed as a hard platform limitation, not a config gap. Fails soft: any error just leaves leadRef null. */ fetchLeadRef: async function (leadId) { if (!this.config.leadRefFlowUrl || !leadId) return null; try { var r = await fetch(this.config.leadRefFlowUrl, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ leadId: leadId }) }); if (!r.ok) { console.warn('[ADA] fetchLeadRef HTTP ' + r.status); return null; } var d = await r.json(); var ref = (d && d.leadRef) || 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; /* Snippet prices are GST-inclusive. Sum them first. */ var baseIncGst = this.round2( cfg.baseFee + (s.additionalSiteCount * cfg.additionalSiteFee) + (s.outreachSiteCount * cfg.outreachFee) ); /* Derive ex-GST for surcharge calculation */ var baseExGst = this.round2(baseIncGst / (1 + cfg.gstRate)); var gst = this.round2(baseIncGst - baseExGst); var surcharge = this.round2(baseExGst * cfg.ccSurchargeRate); /* Total = GST-inclusive base + CC surcharge (surcharge itself is not GST-rated here) */ var total = this.round2(baseIncGst + surcharge); return { base: baseIncGst, 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 }); }; /* Line 1: inc-GST base (no surcharge) */ var elBase = document.getElementById('reviewFeeAmount'); if (elBase) elBase.textContent = fmt(fees.base); /* Line 2: inc-GST + CC surcharge (what user actually pays) */ var elTotal = document.getElementById('reviewFeeTotalAmount'); if (elTotal) elTotal.textContent = fmt(fees.total); /* Surcharge detail */ var elSc = document.getElementById('reviewSurchargeAmount'); if (elSc) elSc.textContent = fmt(fees.surcharge); /* Pay button shows the full payable amount */ var elBtn = document.getElementById('payBtnAmount'); if (elBtn) elBtn.textContent = fmt(fees.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 ════════════════════════════════════════════════════════════ */ /* Builds the embeddable field-set for one Additional Practice / Outreach Location site — same shape as the old buildAccountPayload, minus any API call. These travel inside the Lead's __PLUGIN_META__ blob and are created + N:N associated server-side by PortalLeadCreatePlugin (SYSTEM). */ buildSiteAccountMeta: function (key, accountType) { 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') }; return this.stripEmpty(payload); }, 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') : '', ongc_webresponse: this.buildAdaWebResponseHtml() }; /* Additional Practice / Outreach Location accounts: anonymous has no Append/AppendTo on Lead or Account, so the old create-then-$ref approach always 403'd (EntityPermissionCreateIsMissing — same platform limitation AGPAL hit for practitioners). Embedding them here and letting PortalLeadCreatePlugin create + associate as SYSTEM avoids that entirely — anonymous never touches Account for these at all. formSource: 'ADA' tells the plugin to skip its AGPAL-specific primary Account/Contact matching block (ABN/email/name lookup) — that logic isn't relevant here and would create an unwanted extra Account for this Lead if left on. businessGuid is resolved BEFORE that gate in the plugin, so ongc_Business still gets set here same as AGPAL. */ var pluginMeta = { formSource: 'ADA' }; if (this.config.businessId) pluginMeta.businessGuid = this.config.businessId; var additionalAccounts = []; for (var i = 1; i <= this.state.additionalSiteCount; i++) { additionalAccounts.push(this.buildSiteAccountMeta('add' + i, 'Additional Practice')); } if (additionalAccounts.length) pluginMeta.additionalPracticeAccounts = additionalAccounts; var outreachAccounts = []; for (var j = 1; j <= this.state.outreachSiteCount; j++) { outreachAccounts.push(this.buildSiteAccountMeta('out' + j, 'Outreach Location')); } if (outreachAccounts.length) pluginMeta.outreachLocationAccounts = outreachAccounts; payload['description'] = '__PLUGIN_META__' + JSON.stringify(pluginMeta); return this.stripEmpty(payload); }, /* ── Web response HTML ──────────────────────────────────── */ buildAdaWebResponseHtml: 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 fees = this.calcFees(); var now = new Date().toLocaleString('en-AU', { timeZone: 'Australia/Sydney', dateStyle: 'medium', timeStyle: 'short' }); function row(label, value) { return '' + '' + (label || '') + '' + '' + (value || '\u2014') + '' + ''; } function section(title) { return '' + title + ''; } var html = '
' + '' + '' + ''; html += section('1. Practice Information'); html += row('Practice Name', practiceName); html += row('ABN', g('practiceABN')); html += row('Phone', g('practicePhone')); html += row('Fax', g('practiceFax')); html += row('Website', g('practiceWeb')); html += row('Email', g('practiceEmail')); html += row('Corporate / Group', corporateGroup === 'yes' ? g('corporateName') : 'No'); html += section('2. Accreditation Contact'); html += row('Contact', (g('contactFirst') + ' ' + g('contactLast')).trim()); html += row('Position', g('contactPosition')); html += row('Phone', g('contactPhone')); html += row('Email', g('contactEmail')); html += section('3. Practice Address'); html += row('Street Address', [g('addrLine0'), g('addrLine1'), g('addrCity'), g('addrState'), g('addrPostcode'), 'Australia'].filter(Boolean).join(', ')); html += row('Postal Address', differentPostal ? [g('postalLine0'), g('postalLine1'), g('postalCity'), g('postalState'), g('postalPostcode'), 'Australia'].filter(Boolean).join(', ') : 'Same as street address'); if (this.state.additionalSiteCount > 0) { html += section('4. Additional Practice Sites'); html += row('Count', String(this.state.additionalSiteCount)); for (var i = 1; i <= this.state.additionalSiteCount; i++) { html += ''; html += row('Address', [g('add' + i + '-addr'), g('add' + i + '-city'), g('add' + i + '-state'), g('add' + i + '-postcode')].filter(Boolean).join(', ')); html += row('Phone', g('add' + i + '-phone')); html += row('Email', g('add' + i + '-email')); } } if (this.state.outreachSiteCount > 0) { html += section('5. Outreach Locations'); html += row('Count', String(this.state.outreachSiteCount)); for (var j = 1; j <= this.state.outreachSiteCount; j++) { html += ''; html += row('Address', [g('out' + j + '-addr'), g('out' + j + '-city'), g('out' + j + '-state'), g('out' + j + '-postcode')].filter(Boolean).join(', ')); html += row('Phone', g('out' + j + '-phone')); html += row('Email', g('out' + j + '-email')); } } html += section('6. Fees'); html += row('Base + Sites', 'AUD $' + fees.base.toFixed(2)); html += row('Credit Card Surcharge', 'AUD $' + fees.surcharge.toFixed(2)); html += row('GST', 'AUD $' + fees.gst.toFixed(2)); html += row('Total', 'AUD $' + fees.total.toFixed(2)); html += '
ADA Private Dental Registration
Submitted: ' + now + '
Site ' + i + (g('add' + i + '-name') ? ' \u2014 ' + g('add' + i + '-name') : '') + '
Location ' + j + (g('out' + j + '-name') ? ' \u2014 ' + g('out' + j + '-name') : '') + '
'; return html; }, 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') }; /* NOTE: originatingleadid@odata.bind and ongc_ContactType@odata.bind * were removed at some point (Append To on lead is blocked for * anon) but nothing replaced them — this Contact is currently * created unlinked to the Lead. Flagging this; not fixed as part * of the Additional Practice/Outreach Location $ref fix, since it * wasn't the issue reported. Let me know if you want this wired * up the same way (plugin-side, using the meta blob) too. */ return this.stripEmpty(payload); }, /* ════════════════════════════════════════════════════════════ RECORD CREATION CHAIN ════════════════════════════════════════════════════════════ */ createAllRecords: async function () { /* ── 1. Lead — Additional Practice / Outreach Location accounts are embedded in the description meta blob and created + associated server-side by PortalLeadCreatePlugin (SYSTEM). See buildLeadPayload. ── */ 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); console.log('[ADA] Additional Practice/Outreach Location accounts delegated to server-side plugin.'); /* ── 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. Contact ── */ this.setBusy(true, 'Creating contact record…'); var contactResult = await this.dataversePost('contacts', this.buildContactPayload()); console.log('[ADA] Contact created:', contactResult.id); /* PaymentEvidence is intentionally NOT created here. Same approach as AGPAL registration: creating a "pending" evidence record now and PATCHing it after Stripe returns requires Write on ongc_paymentevidence for anonymous — a real payment-tampering risk (anyone with/guessing another evidence record's GUID could PATCH its status to "paid"). AGPAL never pre-creates evidence or passes a paymentEvidenceId to checkout; the shared /agpal-stripe-return page creates ONE evidence record, once, with the final confirmed Stripe result — anonymous only ever needs Create on ongc_paymentevidence, never Write. Matching that here: no evidence creation, no paymentEvidenceId param below. */ 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 || '', 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. No evidenceId — see note above createAllRecords step 3. */ 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); } 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(); }); }());