/* ════════════════════════════════════════════════════════════════
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 + '
' +
'
' +
'
' +
'
' +
'
' +
'
' +
'
' +
'
' +
'
' +
'
' +
'
' +
'
' +
'
' +
'
' +
'
' +
'
' +
'
Address Line 1
' +
'
' +
'
' +
'';
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(); });
}());