';
}
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 = '
'
+ '
'
+ '
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');
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 += '
';
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(); });
}());