/* ════════════════════════════════════════════════════════════════
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: [0, 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();
var activePage = document.querySelector('.ada-form-page.active');
if (activePage) this.bindLiveValidationClear(activePage);
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 (isCC) {
if (isCC === undefined) isCC = true;
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 = isCC ? this.round2(baseExGst * cfg.ccSurchargeRate) : 0;
/* 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 isCC = this.gVal('paymentMethod') !== 'eft';
var fees = this.calcFees(isCC);
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 — hidden entirely for bank transfer */
var elSc = document.getElementById('reviewSurchargeAmount');
if (elSc) elSc.textContent = fmt(fees.surcharge);
var elScLabel = document.getElementById('feeSurchargeLabel');
if (elScLabel) elScLabel.style.display = isCC ? '' : 'none';
/* Pay button shows the full payable amount and reflects the chosen method */
var elBtn = document.getElementById('payBtnAmount');
if (elBtn) elBtn.textContent = fmt(fees.total);
var payBtnEl = document.getElementById('payBtn');
if (payBtnEl) {
payBtnEl.innerHTML = (isCC ? '🔒 Proceed to Pay — A$' : '📤 Submit Registration — A$')
+ '' + 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 === 0 ? 'Eligibility' : 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;
},
/* ════════════════════════════════════════════════════════════
REQUIRED-FIELD VALIDATION
Generic engine: scans the given page element for any visible
[required] input/select/textarea (and required radio groups),
flags empty ones with a red border + a "Required field" banner
inserted right under the field's label (matching the reference
behaviour), and returns whether the page is valid. Fields inside
a hidden container (display:none) are skipped automatically via
the offsetParent check, so conditional fields (e.g. corporate
group name, non-ADA member name) are only enforced when shown.
════════════════════════════════════════════════════════════ */
clearPageErrors: function (pageEl) {
if (!pageEl) return;
pageEl.querySelectorAll('.ada-invalid').forEach(function (el) { el.classList.remove('ada-invalid'); });
pageEl.querySelectorAll('.ada-required-error').forEach(function (el) { el.remove(); });
},
markFieldInvalid: function (field) {
field.classList.add('ada-invalid');
var group = field.closest('.ada-field-group') || field.parentElement;
if (!group || group.querySelector('.ada-required-error')) return;
var banner = document.createElement('div');
banner.className = 'ada-error-msg ada-required-error';
banner.textContent = 'Required field';
var label = group.querySelector('label');
if (label && label.parentNode === group) {
label.insertAdjacentElement('afterend', banner);
} else {
group.insertBefore(banner, group.firstChild);
}
},
validateRequiredOnPage: function (pageEl) {
if (!pageEl) return true;
this.clearPageErrors(pageEl);
var valid = true;
var self = this;
var firstInvalid = null;
/* Plain inputs / selects / textareas */
pageEl.querySelectorAll('input[required], select[required], textarea[required]').forEach(function (field) {
if (field.type === 'radio' || field.type === 'checkbox') return;
if (field.offsetParent === null) return; /* hidden — not currently applicable */
var val = (field.value || '').trim();
if (!val) {
valid = false;
self.markFieldInvalid(field);
if (!firstInvalid) firstInvalid = field;
}
});
/* Required radio groups */
var seen = {};
pageEl.querySelectorAll('input[type="radio"][required]').forEach(function (r) {
if (seen[r.name]) return;
seen[r.name] = true;
var group = pageEl.querySelectorAll('input[type="radio"][name="' + r.name + '"]');
if (group.length && group[0].offsetParent === null) return;
var checked = false;
group.forEach(function (g) { if (g.checked) checked = true; });
if (!checked) {
valid = false;
self.markFieldInvalid(r);
if (!firstInvalid) firstInvalid = r;
}
});
if (firstInvalid) {
firstInvalid.scrollIntoView({ behavior: 'smooth', block: 'center' });
}
return valid;
},
/* Clear a field's invalid state as soon as the user interacts with it */
bindLiveValidationClear: function (pageEl) {
if (!pageEl || pageEl.dataset.liveValidationBound === '1') return;
pageEl.dataset.liveValidationBound = '1';
pageEl.addEventListener('input', function (e) { ADA.clearFieldError(e.target); });
pageEl.addEventListener('change', function (e) { ADA.clearFieldError(e.target); });
},
clearFieldError: function (field) {
if (!field || !field.classList) return;
if (field.type === 'radio') {
var group = document.querySelectorAll('input[type="radio"][name="' + field.name + '"]');
group.forEach(function (g) { g.classList.remove('ada-invalid'); });
} else {
field.classList.remove('ada-invalid');
}
var fieldGroup = field.closest('.ada-field-group') || field.parentElement;
var banner = fieldGroup && fieldGroup.querySelector('.ada-required-error');
if (banner) banner.remove();
},
/* ════════════════════════════════════════════════════════════
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');
this.bindLiveValidationClear(target);
window.scrollTo({ top: 0, behavior: 'smooth' });
}
this.buildProgress();
},
goToPage: function (n) { this.showPage(n); },
/* ── Page 0 (Eligibility) → Page 1 (Practice Details) ── */
eligibilityNextClick: function () {
var pg = document.querySelector('.ada-form-page[data-page="0"]');
if (!this.validateRequiredOnPage(pg)) return;
var idx = this.state.pageStructure.indexOf(0);
this.showPage(idx + 2);
},
/* ── Page 1 (Practice Details) → Page 2 (Assessment) ── */
practiceDetailsNextClick: function () {
var pg = document.querySelector('.ada-form-page[data-page="1"]');
if (!this.validateRequiredOnPage(pg)) return;
var idx = this.state.pageStructure.indexOf(1);
this.showPage(idx + 2);
},
assessmentNextClick: function () {
var pg = document.querySelector('.ada-form-page[data-page="2"]');
if (!this.validateRequiredOnPage(pg)) return;
var pgIdx = this.state.pageStructure.indexOf(2);
var nextIdx = pgIdx + 1;
/* populateReview() must never block navigation — it does DOM work
(including reinitialising the signature canvas via initSig(),
which had no null-guard) that can throw. An uncaught exception
here previously stopped execution before showPage() ran at all,
making Next appear completely unresponsive. */
if (this.state.pageStructure[nextIdx] === 'review') {
try { this.populateReview(); } catch (e) { console.warn('[ADA] populateReview failed (non-fatal):', e.message); }
}
this.showPage(nextIdx + 1);
},
reviewBack: function () {
var revIdx = this.state.pageStructure.indexOf('review');
this.showPage(revIdx);
},
dynNavNext: function (key) {
var pg = document.querySelector('.ada-form-page[data-dynpage="' + key + '"]');
if (!this.validateRequiredOnPage(pg)) return;
var idx = this.state.pageStructure.indexOf(key);
/* Same guard as assessmentNextClick — see comment there. This is
the path that was actually broken: clicking Next on the last
additional-site/outreach page silently did nothing because
populateReview() threw before showPage() could run. */
if (this.state.pageStructure[idx + 1] === 'review') {
try { this.populateReview(); } catch (e) { console.warn('[ADA] populateReview failed (non-fatal):', e.message); }
}
this.showPage(idx + 2);
},
dynNavPrev: function (key) {
var idx = this.state.pageStructure.indexOf(key);
this.showPage(idx);
},
rebuildPageStructure: function () {
var s = [0, 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
' +
'
' +
'
' +
/* No inline onclick here — Power Pages' CSP allow-lists inline
event handlers by SHA256 hash of content present at server
render time. Handlers injected later via innerHTML never
match a hash and get silently blocked (visible only as a
CSP violation in the console, not a JS error) — this is
exactly what was blocking Next/Previous on these pages.
data-nav-key + addEventListener below sidesteps it entirely
since it's a plain function call from the already-approved
external script, not new inline content. */
'
' +
'' +
'
' +
'' +
'
';
var reviewPage = document.getElementById('reviewPage');
document.getElementById('formShell').insertBefore(el, reviewPage);
var prevBtn = el.querySelector('.ada-dyn-prev');
var nextBtn = el.querySelector('.ada-dyn-next');
if (prevBtn) prevBtn.addEventListener('click', function () { self.dynNavPrev(key); });
if (nextBtn) nextBtn.addEventListener('click', function () { self.dynNavNext(key); });
},
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';
},
/* ════════════════════════════════════════════════════════════
ELIGIBILITY PAGE (Page 0) — CONDITIONAL LOGIC
accreditationCycles: 0 | 1 | 2 (2 = "2 or more")
ivSedation: yes | no
assessmentModel: desktop | virtual | onsite
- IV sedation = Yes -> show NSQHS note, hide assessment model field
- IV sedation = No -> hide NSQHS note, show assessment model field
with options gated by accreditationCycles
("2 or more" excludes Desktop Assessment)
- Assessment model = virtual/onsite -> show "Next Steps" (custom
quote) note; desktop (or unselected) -> hide it
════════════════════════════════════════════════════════════ */
ASSESSMENT_MODEL_OPTIONS: {
desktop: 'Desktop Assessment',
virtual: 'Desktop + Virtual Assessment',
onsite: 'Announced on-site assessment'
},
onCyclesChange: function () {
this.rebuildAssessmentModelOptions();
},
onIvSedationChange: function () {
var v = this.gVal('ivSedation');
var yesNote = document.getElementById('ivSedationYesNote');
var modelWrap = document.getElementById('assessmentModelWrap');
if (yesNote) yesNote.style.display = (v === 'yes') ? 'block' : 'none';
if (modelWrap) modelWrap.style.display = (v === 'no') ? 'block' : 'none';
if (v === 'no') {
this.rebuildAssessmentModelOptions();
} else {
/* Selection no longer applicable — clear it so it can't be
submitted, and hide the Next Steps note that depends on it. */
var sel = document.getElementById('assessmentModel');
if (sel) sel.value = '';
this.toggleNextSteps();
}
},
rebuildAssessmentModelOptions: function () {
var cycles = this.gVal('accreditationCycles');
var sel = document.getElementById('assessmentModel');
var restrictionNote = document.getElementById('assessmentModelRestrictionNote');
if (!sel) return;
var excludeDesktop = (cycles === '2');
if (restrictionNote) restrictionNote.style.display = excludeDesktop ? 'block' : 'none';
var prevValue = sel.value;
var keys = excludeDesktop ? ['virtual', 'onsite'] : ['desktop', 'virtual', 'onsite'];
var self = this;
sel.innerHTML = '' +
keys.map(function (k) { return ''; }).join('');
/* Keep the previous choice only if it's still a valid option
(e.g. cycles changed away from "2 or more" back to 0/1) */
sel.value = (keys.indexOf(prevValue) !== -1) ? prevValue : '';
this.toggleNextSteps();
},
onAssessmentModelChange: function () {
this.toggleNextSteps();
},
toggleNextSteps: function () {
var note = document.getElementById('assessmentNextStepsNote');
if (!note) return;
var v = this.gVal('assessmentModel');
note.style.display = (v === 'virtual' || v === 'onsite') ? 'flex' : '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') + '
';
}
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 && c.parentElement.clientWidth) || 680;
c.height = 110;
/* Clone to remove stale listeners */
var fresh = c.cloneNode(true);
if (!c.parentNode) return;
c.parentNode.replaceChild(fresh, c);
c = fresh;
var ctx = c.getContext && c.getContext('2d');
if (!ctx) { console.warn('[ADA] initSig: 2D context unavailable, signature pad disabled.'); return; }
this.state.sigCtx = ctx;
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,
/* Pipeline (Choice): always 2 = Online Registration for this form */
ongc_pipeline: 2,
/* 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 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 = '
'
+ '
'
+ '
ADA Private Dental Registration
'
+ '
Submitted: ' + now + '
';
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 += '
Site ' + i + (g('add' + i + '-name') ? ' \u2014 ' + g('add' + i + '-name') : '') + '
';
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');
var isCC = g('paymentMethod') !== 'eft';
var wrFees = this.calcFees(isCC);
html += row('Payment Mode', isCC ? 'Credit Card' : 'Bank Transfer');
html += row('Base + Sites', 'AUD $' + wrFees.base.toFixed(2));
html += row('Credit Card Surcharge', 'AUD $' + wrFees.surcharge.toFixed(2));
html += row('GST', 'AUD $' + wrFees.gst.toFixed(2));
html += row('Total', 'AUD $' + wrFees.total.toFixed(2));
html += '
';
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');
var pm = this.gVal('paymentMethod');
var pmErrEl = document.getElementById('paymentMethodError');
var missingPaymentMethod = (pm !== 'cc' && pm !== 'eft');
if (pmErrEl) pmErrEl.style.display = missingPaymentMethod ? 'block' : 'none';
if (!auth || !auth.checked || !tc || !tc.checked || !confirm || !confirm.checked || !af || !al || this.isSigEmpty() || missingPaymentMethod) {
if (errEl) { errEl.style.display = 'flex'; errEl.scrollIntoView({ behavior: 'smooth', block: 'center' }); }
else if (pmErrEl) { pmErrEl.scrollIntoView({ behavior: 'smooth', block: 'center' }); }
return;
}
if (errEl) errEl.style.display = 'none';
try {
/* Create Lead + Accounts + Contact + Evidence */
await this.createAllRecords();
if (pm === 'eft') {
/* Bank transfer: no Stripe session — create the payment evidence
record directly and show the confirmation page. */
await this.completeEftRegistration();
} else {
/* 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);
}
},
/* ════════════════════════════════════════════════════════════
BANK TRANSFER (EFT) — no Stripe session. Create the payment
evidence record directly (Create-only, same anonymous
permission ADA already relies on for Lead/Contact) and show
the confirmation page in place of the Stripe redirect.
════════════════════════════════════════════════════════════ */
completeEftRegistration: async function () {
this.setBusy(true, 'Finalising registration\u2026');
var fees = this.calcFees(false);
var payload = {
ongc_payername: (this.gVal('contactFirst') + ' ' + this.gVal('contactLast')).trim(),
ongc_paymentamount: fees.base,
ongc_surchargefee: fees.surcharge,
ongc_gst: fees.gst,
ongc_totalamount: fees.total,
ongc_paymentstatus: 'pending-eft',
/* Payment Mode (Choice): 107150000 = Credit Card, 107150001 = Bank Transfer */
ongc_paymentmode: 107150001,
ongc_paymentdate: new Date().toISOString()
};
/* ongc_Lead@odata.bind omitted — Append To on lead is blocked for
anonymous. leadId is staged in ongc_description for
AgpalPaymentEvidenceCreatePlugin (SYSTEM), same as every other
evidence record created against this table. */
if (this.state.leadId) payload.ongc_description = JSON.stringify({ __leadId__: this.state.leadId });
await this.dataversePost('ongc_paymentevidences', this.stripEmpty(payload));
this.setBusy(false);
var refEl = document.getElementById('eftRefNumber');
if (refEl) refEl.textContent = this.state.leadRef || (this.state.leadId || '').toUpperCase();
document.querySelectorAll('.ada-form-page').forEach(function (el) { el.classList.remove('active'); });
var successPage = document.getElementById('eftSuccessPage');
if (successPage) { successPage.classList.add('active'); window.scrollTo({ top: 0, behavior: 'smooth' }); }
}
}; /* 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(); });
}());