';
document.body.appendChild(overlay);
}
function setBusy(busy, msg) {
SPAP.isBusy = busy;
var overlay = document.getElementById('spap-spinner-overlay');
var msgEl = document.getElementById('spap-spinner-msg');
if (overlay) overlay.style.display = busy ? 'flex' : 'none';
if (msgEl && msg) msgEl.textContent = msg;
if (msgEl && !busy) msgEl.textContent = 'Please wait\u2026';
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
════════════════════════════════════════════════════════════ */
function waitForToken() {
return new Promise(function (resolve, reject) {
var pc = window.portalContext || {};
var cached = pc.csrfToken || pc.requestVerificationToken;
if (cached && cached.length > 20) { resolve(cached); return; }
var domEl = document.querySelector('input[name="__RequestVerificationToken"]');
if (domEl && domEl.value) { resolve(domEl.value); return; }
var csrfEl = document.getElementById('csrf-token-value');
if (csrfEl && csrfEl.value && csrfEl.value.length > 20) { resolve(csrfEl.value); return; }
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 found')); }
})
.catch(function (e) { reject(new Error('CSRF fetch failed: ' + e.message)); });
}
}, 150);
});
}
/* ════════════════════════════════════════════════════════════
DATAVERSE API
════════════════════════════════════════════════════════════ */
function extractGuid(data, xhrOrResponse) {
if (data && typeof data === 'object') {
var fromBody = data.leadid || data.contactid || data.ongc_paymentevidenceid;
if (!fromBody) {
var odataId = (data['@odata.id']) || '';
var mo = odataId.match(/\(([0-9a-f-]{36})\)/i);
if (mo) fromBody = mo[1];
}
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 m = headerVal.match(/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/i);
if (m) return m[0];
}
return null;
}
function parseApiError(x) {
try { if (x.responseJSON && x.responseJSON.error) return x.responseJSON.error.message; } catch (e) { }
try { var p = JSON.parse(x.responseText); return (p && p.error && p.error.message) || 'Unknown error'; } catch (e) { }
return 'Unknown error';
}
async function dataversePost(entitySet, payload) {
var url = '/_api/' + entitySet;
if (window.webapi && typeof window.webapi.safeAjax === 'function') {
return new Promise(function (resolve, reject) {
window.webapi.safeAjax({
type: 'POST', url: url, contentType: 'application/json',
data: JSON.stringify(payload),
success: function (data, s, xhr) { resolve({ id: extractGuid(data, xhr), data: data || {} }); },
error: function (xhr) { reject(new Error(parseApiError(xhr))); }
});
});
}
var token = await 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: extractGuid(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 always 403'd —
confirmed as a hard platform limitation. Fails soft. */
async function fetchLeadRef(leadId) {
if (!SPAP.config.leadRefFlowUrl || !leadId) return null;
try {
var r = await fetch(SPAP.config.leadRefFlowUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ leadId: leadId })
});
if (!r.ok) { console.warn('[SPAP] fetchLeadRef HTTP ' + r.status); return null; }
var d = await r.json();
var ref = (d && d.leadRef) || null;
console.log('[SPAP] fetchLeadRef:', ref);
return ref;
} catch (e) { console.warn('[SPAP] fetchLeadRef error:', e.message); return null; }
}
/* ════════════════════════════════════════════════════════════
SESSION STORAGE
════════════════════════════════════════════════════════════ */
function clearSession() {
[SPAP.leadIdKey, SPAP.leadRefKey, SPAP.evidenceIdKey, SPAP.paymentInProgressKey].forEach(function (k) {
try { sessionStorage.removeItem(k); } catch (e) { }
});
SPAP.leadId = null; SPAP.leadRef = null; SPAP.paymentEvidenceId = null;
}
function saveSession() {
try {
if (SPAP.leadId) sessionStorage.setItem(SPAP.leadIdKey, SPAP.leadId);
if (SPAP.leadRef) sessionStorage.setItem(SPAP.leadRefKey, SPAP.leadRef);
if (SPAP.paymentEvidenceId) sessionStorage.setItem(SPAP.evidenceIdKey, SPAP.paymentEvidenceId);
} catch (e) { }
}
/* ════════════════════════════════════════════════════════════
HELPERS
════════════════════════════════════════════════════════════ */
function round2(n) { return Math.round(n * 100) / 100; }
function gVal(id) {
var el = document.getElementById(id);
return el ? el.value.trim() : '';
}
function stripEmpty(payload) {
Object.keys(payload).forEach(function (k) {
if (payload[k] === '' || payload[k] === null || payload[k] === undefined) delete payload[k];
});
return payload;
}
/* ════════════════════════════════════════════════════════════
FEE CALCULATION
════════════════════════════════════════════════════════════ */
function getFeeBase() {
return SPAP.config.baseFee
+ (SPAP.additionalSiteCount * SPAP.config.additionalSiteFee)
+ (SPAP.programCount * SPAP.config.programFee);
}
function calcFees(isCC) {
if (isCC === undefined) isCC = true;
var base = getFeeBase();
var surcharge = isCC ? round2(base * SPAP.config.ccSurchargeRate) : 0;
var gst = round2((base + surcharge) * SPAP.config.gstRate);
var total = round2(base + surcharge + gst);
return { base: round2(base), surcharge: surcharge, gst: gst, total: total };
}
function spapCalculateFee() {
var base = getFeeBase();
var fmt = base.toLocaleString('en-AU', { minimumFractionDigits: 2, maximumFractionDigits: 2 });
var el = document.getElementById('spapFeeAmount');
if (el) el.textContent = fmt;
/* Populate surcharge % display from portalContext — avoids Liquid math on string values */
var surEl = document.getElementById('spapSurchargeDisplay');
if (surEl) {
var rate = parseFloat(SPAP.config.ccSurchargeRate) || 0.01927;
var pct = (rate * 100).toFixed(3).replace(/\.?0+$/, ''); /* e.g. "1.927" */
surEl.textContent = pct;
}
/* Hide the CC-surcharge banner entirely for bank transfer, and
relabel the final submit button to match the chosen method. */
var isCC = gVal('spapPaymentMethod') !== 'eft';
var banner = document.getElementById('spapSurchargeBanner');
if (banner) banner.style.display = isCC ? '' : 'none';
var submitBtn = document.getElementById('spapSubmitBtn');
if (submitBtn) {
submitBtn.innerHTML = isCC
? '🔒 Proceed to Payment'
: '📤 Submit Registration';
}
}
/* ════════════════════════════════════════════════════════════
PAGE STRUCTURE
════════════════════════════════════════════════════════════ */
function spapRebuildStructure() {
var s = ['1', '2', '3'];
for (var i = 1; i <= SPAP.additionalSiteCount; i++) s.push('site' + i);
for (var j = 1; j <= SPAP.programCount; j++) s.push('prog' + j);
s.push('fee');
s.push('submit');
SPAP.pageStructure = s;
spapBuildProgress();
spapCalculateFee();
}
/* ════════════════════════════════════════════════════════════
PROGRESS BAR
════════════════════════════════════════════════════════════ */
function spapBuildProgress() {
var c = document.getElementById('spapProgressSteps');
if (!c) return;
c.innerHTML = '';
var total = SPAP.pageStructure.length;
var currIdx = SPAP.pageStructure.indexOf(SPAP.currentPageKey);
SPAP.pageStructure.forEach(function (key, idx) {
var isCurr = SPAP.currentPageKey === key;
var isDone = currIdx > idx;
var label = SPAP.LABELS[key] || key;
var div = document.createElement('div');
div.className = 'spap-step-item' + (isCurr ? ' active' : '') + (isDone ? ' done' : '');
div.innerHTML =
'
' + (isDone ? '✓' : (idx + 1)) + '
' +
'
' + label + '
';
c.appendChild(div);
});
var counter = document.getElementById('spapPageCounter');
if (counter) counter.textContent = 'Page ' + (currIdx + 1) + ' of ' + total;
}
/* ════════════════════════════════════════════════════════════
PAGE NAVIGATION
════════════════════════════════════════════════════════════ */
function spapShowByKey(key) {
if (SPAP.isBusy) return;
SPAP.currentPageKey = key;
document.querySelectorAll('.spap-form-page').forEach(function (el) { el.classList.remove('active'); });
var target;
if (key === 'fee') { target = document.getElementById('spapFeePage'); }
else if (key === 'submit') { target = document.getElementById('spapSubmitPage'); }
else if (key.indexOf('site') === 0 || key.indexOf('prog') === 0) {
target = document.querySelector('.spap-form-page[data-dynkey="' + key + '"]');
} else {
target = document.querySelector('.spap-form-page[data-page="' + key + '"]');
}
if (target) { target.classList.add('active'); window.scrollTo({ top: 0, behavior: 'smooth' }); }
spapBuildProgress();
if (key === 'submit') spapInitSig();
if (key === 'fee') spapCalculateFee();
}
function spapNavNext(currentKey) {
var idx = SPAP.pageStructure.indexOf(currentKey);
if (idx >= 0 && idx < SPAP.pageStructure.length - 1) spapShowByKey(SPAP.pageStructure[idx + 1]);
}
function spapNavPrev(currentKey) {
var idx = SPAP.pageStructure.indexOf(currentKey);
if (idx > 0) spapShowByKey(SPAP.pageStructure[idx - 1]);
}
function spapGoToPage(n) { spapShowByKey(String(n)); }
function spapPage3Next() { spapNavNext('3'); }
function spapFeeNext() { spapNavNext('fee'); }
function spapFeeBack() { spapNavPrev('fee'); }
function spapSubmitBack() { spapNavPrev('submit'); }
/* ════════════════════════════════════════════════════════════
CONDITIONAL UI
════════════════════════════════════════════════════════════ */
function spapToggleSector() {
var v = gVal('spapSector');
var fld = document.getElementById('spapSectorOtherField');
if (fld) fld.style.display = (v === 'other') ? 'block' : 'none';
}
function spapTogglePostal(val) {
var yesChk = document.getElementById('spapPostalYes');
var noChk = document.getElementById('spapPostalNo');
var block = document.getElementById('spapPostalBlock');
if (val === 'yes') {
if (yesChk) yesChk.checked = true;
if (noChk) noChk.checked = false;
if (block) block.style.display = 'block';
} else {
if (yesChk) yesChk.checked = false;
if (noChk) noChk.checked = true;
if (block) block.style.display = 'none';
}
}
/* ════════════════════════════════════════════════════════════
ADDITIONAL SITES HANDLER
════════════════════════════════════════════════════════════ */
function spapOnAdditionalSitesChange() {
var v = gVal('spapAdditionalSites');
var hintEl = document.getElementById('spapSiteHint');
var over5El = document.getElementById('spapSiteOver5Msg');
if (hintEl) hintEl.style.display = 'none';
if (over5El) over5El.style.display = 'none';
spapRemoveDynPages('site');
SPAP.additionalSiteCount = 0;
if (v === '0') {
if (hintEl) hintEl.style.display = 'block';
} else if (v === '5plus') {
if (hintEl) hintEl.style.display = 'block';
if (over5El) over5El.style.display = 'block';
} else if (v !== '') {
var count = parseInt(v) || 0;
if (hintEl) hintEl.style.display = 'block';
SPAP.additionalSiteCount = count;
spapBuildSitePages(count);
}
spapRebuildStructure();
}
function spapBuildSitePages(count) {
var formShell = document.getElementById('spapFormShell');
var feePage = document.getElementById('spapFeePage');
var stateOpts = spapStateOptions();
for (var i = 1; i <= count; i++) {
var key = 'site' + i;
if (document.querySelector('.spap-form-page[data-dynkey="' + key + '"]')) continue;
var ord = SPAP.ORDINALS[i - 1] || (i + 'TH');
var siteN = i + 1;
var pg = document.createElement('div');
pg.className = 'spap-form-page';
pg.setAttribute('data-dynkey', key);
pg.innerHTML =
'
' +
'
' + ord + ' ADDITIONAL SITE (SITE ' + siteN + ')
' +
spapFieldWrap('Site ' + siteN + ' name', true,
'') +
spapFieldWrap('Site ' + siteN + ' street address', true,
'' +
'
Address Line 1
' +
'' +
'
' +
'
City
' +
'
State
' +
'
Postcode
' +
'
') +
spapFieldWrap('Management level on site at Site ' + siteN, true,
'') +
spapFieldWrap('Site ' + siteN + ': Preferred Accreditation Contact Name', true,
'
' +
/* No inline onclick — 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 (a CSP violation in
console, not a JS error) — same root cause as the ADA form's
Next/Previous bug. data-nav-key + addEventListener below
sidesteps it since it's a function call from the already-
approved external script, not new inline content. */
'
' +
'' +
'
' +
'' +
'
';
formShell.insertBefore(pg, feePage);
var sitePrevBtn = pg.querySelector('.spap-dyn-prev');
var siteNextBtn = pg.querySelector('.spap-dyn-next');
if (sitePrevBtn) sitePrevBtn.addEventListener('click', function () { spapNavPrev(key); });
if (siteNextBtn) siteNextBtn.addEventListener('click', function () { spapNavNext(key); });
}
}
/* ════════════════════════════════════════════════════════════
PROGRAM COUNT HANDLER
════════════════════════════════════════════════════════════ */
function spapOnProgramCountChange() {
var v = gVal('spapProgramCount');
var over5El = document.getElementById('spapProgOver5Msg');
if (over5El) over5El.style.display = 'none';
spapRemoveDynPages('prog');
SPAP.programCount = 0;
if (v === 'more') {
if (over5El) over5El.style.display = 'block';
} else if (v !== '') {
SPAP.programCount = parseInt(v) || 0;
spapBuildProgramPages(SPAP.programCount);
}
spapRebuildStructure();
}
function spapBuildProgramPages(count) {
var formShell = document.getElementById('spapFormShell');
var feePage = document.getElementById('spapFeePage');
for (var i = 1; i <= count; i++) {
var key = 'prog' + i;
if (document.querySelector('.spap-form-page[data-dynkey="' + key + '"]')) continue;
var pg = document.createElement('div');
pg.className = 'spap-form-page';
pg.setAttribute('data-dynkey', key);
pg.innerHTML =
'
Please enter the site names this program is delivered to.
') +
spapFieldWrap('FTE of staff delivering services under Program ' + i, false,
'') +
'
' +
'
' +
'' +
'
' +
'' +
'
';
formShell.insertBefore(pg, feePage);
var progPrevBtn = pg.querySelector('.spap-dyn-prev');
var progNextBtn = pg.querySelector('.spap-dyn-next');
if (progPrevBtn) progPrevBtn.addEventListener('click', function () { spapNavPrev(key); });
if (progNextBtn) progNextBtn.addEventListener('click', function () { spapNavNext(key); });
}
}
function spapRemoveDynPages(prefix) {
document.querySelectorAll('.spap-form-page[data-dynkey^="' + prefix + '"]').forEach(function (el) { el.remove(); });
}
/* ════════════════════════════════════════════════════════════
HTML HELPERS
════════════════════════════════════════════════════════════ */
function spapFieldWrap(label, required, inner) {
return '
' + inner + '
';
}
function spapStateOptions() {
return '' +
'' +
'' +
'' +
'';
}
function spapPrefixOptions() {
return '' +
'' +
'';
}
/* ════════════════════════════════════════════════════════════
SIGNATURE CANVAS
════════════════════════════════════════════════════════════ */
function spapInitSig() {
var c = document.getElementById('spapSigCanvas');
if (!c) return;
var fresh = c.cloneNode(true);
c.parentNode.replaceChild(fresh, c);
c = fresh;
c.width = c.parentElement.clientWidth || 680;
c.height = 110;
SPAP.sigCtx = c.getContext('2d');
SPAP.sigCtx.strokeStyle = '#1A2B35';
SPAP.sigCtx.lineWidth = 2.2;
SPAP.sigCtx.lineCap = 'round';
SPAP.sigCtx.lineJoin = 'round';
var ctx = SPAP.sigCtx;
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) { SPAP.sigDrawing = true; ctx.beginPath(); var p = pos(e); ctx.moveTo(p.x, p.y); });
c.addEventListener('mousemove', function (e) { if (!SPAP.sigDrawing) return; var p = pos(e); ctx.lineTo(p.x, p.y); ctx.stroke(); });
c.addEventListener('mouseup', function () { SPAP.sigDrawing = false; });
c.addEventListener('mouseleave', function () { SPAP.sigDrawing = false; });
c.addEventListener('touchstart', function (e) { e.preventDefault(); SPAP.sigDrawing = true; ctx.beginPath(); var p = pos(e); ctx.moveTo(p.x, p.y); }, { passive: false });
c.addEventListener('touchmove', function (e) { e.preventDefault(); if (!SPAP.sigDrawing) return; var p = pos(e); ctx.lineTo(p.x, p.y); ctx.stroke(); }, { passive: false });
c.addEventListener('touchend', function () { SPAP.sigDrawing = false; });
}
function spapClrSig() {
var c = document.getElementById('spapSigCanvas');
if (SPAP.sigCtx && c) SPAP.sigCtx.clearRect(0, 0, c.width, c.height);
}
function spapIsSigEmpty() {
var c = document.getElementById('spapSigCanvas');
if (!c || !SPAP.sigCtx) return true;
var d = SPAP.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
════════════════════════════════════════════════════════════ */
function buildLeadPayload() {
var orgName = gVal('spapOrgName');
var yesChk = document.getElementById('spapPostalYes');
var diffPostal = yesChk ? yesChk.checked : false;
/* Collect programs summary for description */
var programs = [];
for (var i = 1; i <= SPAP.programCount; i++) {
programs.push({
name: gVal('spapP' + i + 'Name'),
sites: gVal('spapP' + i + 'Sites'),
fte: parseFloat(gVal('spapP' + i + 'FTE')) || 0
});
}
/* Collect additional sites summary */
var sites = [];
for (var s = 1; s <= SPAP.additionalSiteCount; s++) {
sites.push({
name: gVal('spapSite' + s + 'Name'),
city: gVal('spapSite' + s + 'City'),
state: gVal('spapSite' + s + 'State'),
postcode: gVal('spapSite' + s + 'Postcode')
});
}
var payload = {
/* Lead subject = page title */
subject: 'QIP Suicide Prevention Accreditation Program Registration \u2013 ' + orgName,
/* Accreditation contact fields map to Lead owner */
firstname: gVal('spapContactFirst'),
lastname: gVal('spapContactLast') || orgName,
jobtitle: gVal('spapContactPosition'),
emailaddress1: gVal('spapContactEmail'),
telephone1: gVal('spapContactPhone'),
/* Organisation fields */
companyname: orgName,
fax: gVal('spapOrgFax'),
websiteurl: gVal('spapOrgWebsite'),
mobilephone: gVal('spapOrgPhone'),
leadsourcecode: 8,
/* Street address */
address1_line1: gVal('spapStreetAddr0'),
address1_line2: gVal('spapStreetAddr1'),
address1_city: gVal('spapStreetCity'),
address1_stateorprovince: gVal('spapStreetState'),
address1_postalcode: gVal('spapStreetPostcode'),
address1_country: 'Australia',
/* Postal address */
address2_line1: diffPostal ? gVal('spapPostalAddr0') : '',
address2_line2: diffPostal ? gVal('spapPostalAddr1') : '',
address2_city: diffPostal ? gVal('spapPostalCity') : '',
address2_stateorprovince: diffPostal ? gVal('spapPostalState') : '',
address2_postalcode: diffPostal ? gVal('spapPostalPostcode') : '',
address2_country: diffPostal ? 'Australia' : '',
/* Custom ONGC fields */
ongc_enquirytype: 3, /* 3 = QIP SPAP */
/* Pipeline (Choice): always 2 = Online Registration for this form */
ongc_pipeline: 2,
ongc_practicename: orgName,
ongc_practiceabn: gVal('spapOrgABN'),
ongc_practicephone: gVal('spapOrgPhone'),
ongc_postalsameasstreet: !diffPostal,
ongc_webresponse: spapBuildWebResponseHtml(programs, sites)
};
/* __PLUGIN_META__ blob (same mechanism as AGPAL/ADA) — businessGuid is
resolved server-side by PortalLeadCreatePlugin before any
formSource gating, same as ADA. formSource: 'SPAP' skips the
plugin's AGPAL-specific primary Account/Contact matching block —
SPAP doesn't use that concept and already creates its own Contact
client-side.
NOTE: Additional Sites / Programs are NOT yet embedded as
associable records here — that requires the target entity type and
the Lead<->entity N:N relationship schema names, which aren't
available yet. For now they're captured in ongc_webresponse only
(see spapBuildWebResponseHtml), same as before this fix, just
formatted instead of raw JSON text. */
var pluginMeta = { formSource: 'SPAP' };
if (SPAP.config.businessId) pluginMeta.businessGuid = SPAP.config.businessId;
payload['description'] = '__PLUGIN_META__' + JSON.stringify(pluginMeta);
return stripEmpty(payload);
}
/* ── Web response HTML ──────────────────────────────────── */
function spapBuildWebResponseHtml(programs, sites) {
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 isCC = gVal('spapPaymentMethod') !== 'eft';
var fees = calcFees(isCC);
var yesChk = document.getElementById('spapPostalYes');
var diffPostal = yesChk ? yesChk.checked : false;
var html = '
'
+ '
'
+ '
QIP Suicide Prevention Accreditation Program Registration
'
+ '
Submitted: ' + now + '
';
html += section('1. Organisation');
html += row('Organisation Name', gVal('spapOrgName'));
html += row('ABN', gVal('spapOrgABN'));
html += row('Phone', gVal('spapOrgPhone'));
html += row('Fax', gVal('spapOrgFax'));
html += row('Website', gVal('spapOrgWebsite'));
html += row('Sector', gVal('spapSector') + (gVal('spapSectorOther') ? ' (' + gVal('spapSectorOther') + ')' : ''));
html += row('Grant', gVal('spapGrant'));
html += row('Management Level', gVal('spapManagementLevel'));
html += section('2. Accreditation Contact');
html += row('Contact', (gVal('spapContactFirst') + ' ' + gVal('spapContactLast')).trim());
html += row('Position', gVal('spapContactPosition'));
html += row('Phone', gVal('spapContactPhone'));
html += row('Email', gVal('spapContactEmail'));
html += section('3. Address');
html += row('Street Address', [gVal('spapStreetAddr0'), gVal('spapStreetAddr1'), gVal('spapStreetCity'), gVal('spapStreetState'), gVal('spapStreetPostcode'), 'Australia'].filter(Boolean).join(', '));
html += row('Postal Address', diffPostal
? [gVal('spapPostalAddr0'), gVal('spapPostalAddr1'), gVal('spapPostalCity'), gVal('spapPostalState'), gVal('spapPostalPostcode'), 'Australia'].filter(Boolean).join(', ')
: 'Same as street address');
if (programs && programs.length) {
html += section('4. Programs');
html += row('Count', String(programs.length));
programs.forEach(function (p, i) {
html += '
Program ' + (i + 1) + (p.name ? ' \u2014 ' + p.name : '') + '
';
html += row('Sites', p.sites);
html += row('FTE', p.fte);
});
}
if (sites && sites.length) {
html += section('5. Additional Sites');
html += row('Count', String(sites.length));
sites.forEach(function (s, i) {
html += '
Site ' + (i + 1) + (s.name ? ' \u2014 ' + s.name : '') + '
';
html += row('Address', [s.city, s.state, s.postcode].filter(Boolean).join(', '));
});
}
html += section('6. Fees');
html += row('Payment Mode', isCC ? 'Credit Card' : 'Bank Transfer');
html += row('Base + Additional', '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;
}
function buildContactPayload() {
var payload = {
firstname: gVal('spapContactFirst'),
lastname: gVal('spapContactLast'),
jobtitle: gVal('spapContactPosition'),
emailaddress1: gVal('spapContactEmail'),
telephone1: gVal('spapContactPhone'),
mobilephone: gVal('spapOrgPhone')
};
/* originatingleadid and ongc_ContactType @odata.bind removed.
* Contact create/link handled by PortalLeadCreatePlugin as SYSTEM. */
return stripEmpty(payload);
}
/* ════════════════════════════════════════════════════════════
RECORD CREATION CHAIN
════════════════════════════════════════════════════════════ */
async function createAllRecords() {
/* ── 1. Lead — critical ── */
setBusy(true, 'Creating registration record\u2026');
var lead = await dataversePost('leads', buildLeadPayload());
SPAP.leadId = lead.id;
if (!SPAP.leadId) throw new Error('Lead created but GUID not returned. Check Dataverse Web API permissions for the lead entity.');
console.log('[SPAP] Lead created:', SPAP.leadId);
/* ── 2. Fetch autonumber reference ── */
setBusy(true, 'Fetching registration reference\u2026');
var fetchedRef = await fetchLeadRef(SPAP.leadId);
SPAP.leadRef = fetchedRef || null;
console.log('[SPAP] leadRef:', SPAP.leadRef);
saveSession();
/* ── 3. Contact — non-blocking ── */
setBusy(true, 'Creating contact record\u2026');
try {
var cr = await Promise.allSettled([dataversePost('contacts', buildContactPayload())]);
if (cr[0].status === 'rejected') console.warn('[SPAP] Contact failed (non-fatal):', cr[0].reason && cr[0].reason.message);
else console.log('[SPAP] Contact created:', cr[0].value && cr[0].value.id);
} catch (e) { console.warn('[SPAP] Contact error (non-fatal):', e.message); }
/* 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. */
saveSession();
}
/* ════════════════════════════════════════════════════════════
STRIPE REDIRECT (shared /agpal-stripe-checkout page)
════════════════════════════════════════════════════════════ */
function initiateStripePayment() {
var fees = calcFees();
var params = new URLSearchParams({
leadId: SPAP.leadId || '',
leadRef: SPAP.leadRef || '',
amount: fees.total.toFixed(2),
base: fees.base.toFixed(2),
surcharge: fees.surcharge.toFixed(2),
gst: fees.gst.toFixed(2),
email: gVal('spapContactEmail'),
name: (gVal('spapContactFirst') + ' ' + gVal('spapContactLast')).trim(),
ref: 'QIP Suicide Prevention Accreditation Program Registration \u2013 ' + gVal('spapOrgName'),
returnUrl: window.location.href.split('?')[0]
});
/* Persist before redirect — /agpal-stripe-return reads these.
No evidenceId — see note above createAllRecords. */
try {
sessionStorage.setItem(SPAP.paymentInProgressKey, '1');
if (SPAP.leadId) sessionStorage.setItem(SPAP.leadIdKey, SPAP.leadId);
if (SPAP.leadRef) sessionStorage.setItem(SPAP.leadRefKey, SPAP.leadRef);
} catch (e) { }
/* location.replace prevents Back returning to mid-payment form */
window.location.replace('/agpal-stripe-checkout?' + params.toString());
}
/* ════════════════════════════════════════════════════════════
MAIN SUBMIT ENTRY POINT
════════════════════════════════════════════════════════════ */
async function spapSubmitForm() {
var auth = document.getElementById('spapChkAuth') && document.getElementById('spapChkAuth').checked;
var tc = document.getElementById('spapChkTC') && document.getElementById('spapChkTC').checked;
var conf = document.getElementById('spapChkConfirm') && document.getElementById('spapChkConfirm').checked;
var af = gVal('spapAuthFirst');
var al = gVal('spapAuthLast');
var errEl = document.getElementById('spapSubmitError');
var pm = gVal('spapPaymentMethod');
var pmOk = (pm === 'cc' || pm === 'eft');
var pmErrEl = document.getElementById('spapPaymentMethodError');
if (pmErrEl) pmErrEl.style.display = pmOk ? 'none' : 'block';
if (!auth || !tc || !conf || !af || !al || spapIsSigEmpty() || !pmOk) {
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 {
await createAllRecords();
if (pm === 'eft') {
await completeEftRegistration();
} else {
setBusy(true, 'Redirecting to payment\u2026');
initiateStripePayment();
}
} catch (err) {
console.error('[SPAP] Submit failed:', err);
if (errEl) {
errEl.style.display = 'flex';
errEl.textContent = 'Could not submit registration: ' + (err.message || 'Unknown error') + '. Please try again.';
errEl.scrollIntoView({ behavior: 'smooth', block: 'center' });
}
setBusy(false);
}
}
/* ════════════════════════════════════════════════════════════
BANK TRANSFER (EFT) — no Stripe session. Create the payment
evidence record directly (Create-only, same anonymous
permission relied on for Lead/Contact) and show a dedicated
confirmation page in place of the Stripe redirect.
════════════════════════════════════════════════════════════ */
async function completeEftRegistration() {
setBusy(true, 'Finalising registration\u2026');
var fees = calcFees(false);
var payload = {
ongc_payername: (gVal('spapContactFirst') + ' ' + gVal('spapContactLast')).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 (SPAP.leadId) payload.ongc_description = JSON.stringify({ __leadId__: SPAP.leadId });
await dataversePost('ongc_paymentevidences', stripEmpty(payload));
setBusy(false);
var refEl = document.getElementById('spapEftRefNumber');
if (refEl) refEl.textContent = SPAP.leadRef || (SPAP.leadId || '').toUpperCase();
document.querySelectorAll('.spap-form-page').forEach(function (el) { el.classList.remove('active'); });
var successPage = document.getElementById('spapEftSuccessPage');
if (successPage) { successPage.classList.add('active'); window.scrollTo({ top: 0, behavior: 'smooth' }); }
}
/* ════════════════════════════════════════════════════════════
INIT
════════════════════════════════════════════════════════════ */
document.addEventListener('DOMContentLoaded', function () {
injectSpinner();
clearSession();
var noChk = document.getElementById('spapPostalNo');
if (noChk) noChk.checked = true;
spapRebuildStructure();
});
window.addEventListener('resize', function () { if (SPAP.sigCtx) spapInitSig(); });
/* ── Expose all globals used by inline onclick handlers in HTML ── */
window.SPAP = SPAP;
window.spapGoToPage = spapGoToPage;
window.spapNavNext = spapNavNext;
window.spapNavPrev = spapNavPrev;
window.spapPage3Next = spapPage3Next;
window.spapFeeNext = spapFeeNext;
window.spapFeeBack = spapFeeBack;
window.spapSubmitBack = spapSubmitBack;
window.spapToggleSector = spapToggleSector;
window.spapTogglePostal = spapTogglePostal;
window.spapOnAdditionalSitesChange = spapOnAdditionalSitesChange;
window.spapOnProgramCountChange = spapOnProgramCountChange;
window.spapCalculateFee = spapCalculateFee;
window.spapClrSig = spapClrSig;
window.spapSubmitForm = spapSubmitForm;
}());