(function () {
'use strict';
var App = {
state: {
currentStep: 1,
totalSteps: 8,
isBusy: false,
leadId: null,
leadRef: null,
formCacheKey: 'agpal_registration_form',
leadIdKey: 'agpal_registration_lead_id',
leadRefKey: 'agpal_registration_lead_ref',
practitionerRowCount: 0
},
ctx: window.portalContext || {},
/* Config */
config: {
fee: parseFloat((window.portalContext || {}).fee) || 1200,
ccSurchargeRate: parseFloat((window.portalContext || {}).ccSurchargeRate) || 0.01927,
gstRate: parseFloat((window.portalContext || {}).gstRate) || 0.10,
contactTypeId: (window.portalContext || {}).contactTypeId || '',
businessId: (window.portalContext || {}).businessId || '',
operationalTatDays: parseInt((window.portalContext || {}).operationalTatDays, 10) || 14,
practiceTypeGuids: {
gp: (window.portalContext || {}).practiceTypeGP || '',
ahs: (window.portalContext || {}).practiceTypeAHS || '',
mds: (window.portalContext || {}).practiceTypeMDS || '',
aboriginal: (window.portalContext || {}).practiceTypeAboriginal || ''
},
practitionerTypeMap: { registrar: 1, vocational: 2, other: 10 },
/* Power Automate HTTP-trigger URL that looks up ongc_leadrefid by
Lead GUID. Anonymous has no Read on Lead — see notes above
createLeadWithRef — so this is the only channel that can return
it. Set via portalContext (page/site setting) rather than
hardcoded here, so it can be swapped per-environment (Dev/UAT/Prod)
without a code change. */
leadRefFlowUrl: (window.portalContext || {}).leadRefFlowUrl || ''
},
STATE_MAP: {
'Australian Capital Territory': 107150000,
'New South Wales': 107150001,
'Northern Territory': 107150002,
'Queensland': 107150003,
'South Australia': 107150004,
'Tasmania': 107150005,
'Victoria': 107150006,
'Western Australia': 107150007,
/* short-code fallbacks */
'ACT': 107150000, 'NSW': 107150001, 'NT': 107150002,
'QLD': 107150003, 'SA': 107150004, 'TAS': 107150005,
'VIC': 107150006, 'WA': 107150007
},
mapState: function (v) {
if (!v) return undefined;
var n = this.STATE_MAP[v];
if (n !== undefined) return n;
n = this.STATE_MAP[(v + '').trim()];
return n !== undefined ? n : undefined;
},
/* Pending Registration — 14-day threshold
Returns true when:
- accred-status is "no" (new practice, not yet accredited)
- commencement date is more than 14 days from today
*/
isPendingRegistration: function () {
var accred = this.val('accred-status');
if (accred !== 'no') return false;
var dateStr = this.val('commencement-date');
if (!dateStr) return false;
var commenceDate = new Date(dateStr);
if (isNaN(commenceDate.getTime())) return false;
var today = new Date();
today.setHours(0, 0, 0, 0);
var diffMs = commenceDate.getTime() - today.getTime();
var diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24));
return diffDays > this.config.operationalTatDays;
},
dom: {},
/* Init */
init: function () {
this.injectSpinnerOverlay();
this.cacheDom();
this.clearSession();
this.bindEvents();
this.handleConditionalUI();
this.updateFeeTable();
this.updateSubmitEnabled();
console.log('[AGPAL] App initialized');
},
cacheDom: function () {
var ids = [
'stepTitle', 'stepCount', 'progressFill',
'btn-submit', 'btn-submit-agpal',
'lead-status-msg', 'lead-card', 'lead-card-content',
'ref-number', 'toast', 'toast-msg',
'payment-method', 'practice-name', 'practice-email',
'group-accred', 'corporate-entity-name',
'practitioners', 'practitioner-dynamic-section',
'practitioner-over10-section', 'practitioner-rows-container',
'surcharge-row', 'gst-row', 'total-row'
];
for (var i = 0; i < ids.length; i++) {
this.dom[ids[i]] = document.getElementById(ids[i]);
}
},
byId: function (id) { return document.getElementById(id); },
val: function (id) {
var el = this.byId(id);
return el && typeof el.value === 'string' ? el.value.trim() : '';
},
/* ── Toast ──────────────────────────────────────────────── */
showToast: function (message, isError) {
if (!this.dom.toast || !this.dom['toast-msg']) return;
this.dom['toast-msg'].textContent = message;
this.dom.toast.classList.toggle('toast-error', !!isError);
this.dom.toast.classList.add('show');
setTimeout(function () {
if (App.dom.toast) App.dom.toast.classList.remove('show');
}, 4000);
},
injectSpinnerOverlay: function () {
if (document.getElementById('agpal-spinner-overlay')) return;
var style = document.createElement('style');
style.textContent = '@keyframes agpal-spin{to{transform:rotate(360deg)}}'
+ '#agpal-spinner-overlay{display:none;position:fixed;inset:0;background:rgba(26,46,74,0.60);'
+ '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 = 'agpal-spinner-overlay';
overlay.setAttribute('aria-live', 'assertive');
overlay.innerHTML =
'
'
+ '
Please wait…
';
document.body.appendChild(overlay);
},
/* Busy */
setBusy: function (busy, buttonText) {
this.state.isBusy = busy;
var overlay = document.getElementById('agpal-spinner-overlay');
var msgEl = document.getElementById('agpal-spinner-msg');
if (overlay) overlay.style.display = busy ? 'flex' : 'none';
if (msgEl && buttonText) msgEl.textContent = buttonText;
if (msgEl && !busy) msgEl.textContent = 'Please wait…';
var buttons = document.querySelectorAll('button');
buttons.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';
}
});
if (!busy && this.dom['btn-submit']) {
this.dom['btn-submit'].disabled = !this.areDeclarationsChecked();
}
},
/* CSRF */
waitForToken: function () {
return new Promise(function (resolve, reject) {
var cached = (window.portalContext || {}).csrfToken
|| (window.portalContext || {}).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 pc = (window.portalContext || {}).csrfToken;
if (pc && pc.length > 20) { clearInterval(poll); resolve(pc); 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: ' + e.message)); });
}
}, 150);
});
},
/* API helpers */
parseApiError: function (xhr) {
try { if (xhr && xhr.responseJSON && xhr.responseJSON.error) return xhr.responseJSON.error.message || 'Unknown error'; } catch (e1) { }
try { if (xhr && xhr.responseText) { var p = JSON.parse(xhr.responseText); return (p && p.error && p.error.message) || 'Unknown error'; } } catch (e2) { }
return 'Unknown error';
},
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, response) {
if (data && typeof data === 'object') {
var fromBody = data.leadid || data.accountid || data.contactid
|| data.ongc_practitionerid || data.ongc_paymentevidenceid
|| this.extractGuidFromOdata(data);
if (fromBody) return fromBody;
}
var hv = '';
if (response) {
if (typeof response.headers === 'object' && typeof response.headers.get === 'function') {
hv = response.headers.get('OData-EntityId') || '';
}
if (!hv && typeof response.getResponseHeader === 'function') {
hv = response.getResponseHeader('OData-EntityId') || response.getResponseHeader('entityid') || '';
}
}
if (hv) { var m = hv.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;
},
/* POST */
dataversePost: async function (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, status, xhr) { resolve({ id: App.extractEntityId(data, xhr), data: data || {} }); },
error: function (xhr) { reject(new Error(App.parseApiError(xhr))); }
});
});
}
var token = await this.waitForToken();
var resp = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json', '__RequestVerificationToken': token,
'OData-MaxVersion': '4.0', 'OData-Version': '4.0'
},
body: JSON.stringify(payload)
});
if (!resp.ok) {
var em = 'API error ' + resp.status;
try { var ej = await resp.json(); em = (ej && ej.error && ej.error.message) || em; } catch (e) { }
throw new Error(em);
}
var data = {};
try { data = await resp.json(); } catch (e) { }
return { id: this.extractEntityId(data, resp), data: data };
},
/* Misc */
round2: function (n) { return Math.round(n * 100) / 100; },
clearSession: function () {
try {
sessionStorage.removeItem(this.state.formCacheKey);
sessionStorage.removeItem(this.state.leadIdKey);
sessionStorage.removeItem(this.state.leadRefKey);
} catch (e) { }
this.state.leadId = null;
this.state.leadRef = null;
},
saveState: function () {
try {
sessionStorage.setItem(this.state.formCacheKey, JSON.stringify(this.collectFormData()));
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) { }
},
resetForm: function () {
try {
sessionStorage.removeItem(this.state.formCacheKey);
sessionStorage.removeItem(this.state.leadIdKey);
sessionStorage.removeItem(this.state.leadRefKey);
} catch (e) { }
document.querySelectorAll('input, select, textarea').forEach(function (el) {
if (el.type === 'checkbox' || el.type === 'radio') { el.checked = el.defaultChecked; }
else { el.value = el.defaultValue || ''; }
});
var c = document.getElementById('practitioner-rows-container');
if (c) c.innerHTML = '';
document.querySelectorAll('.error-msg').forEach(function (el) { el.classList.remove('visible'); });
if (this.dom['lead-card']) this.dom['lead-card'].style.display = 'none';
if (this.dom['lead-status-msg']) this.dom['lead-status-msg'].style.display = 'none';
if (this.dom['ref-number']) this.dom['ref-number'].textContent = '\u2014';
if (this.dom['btn-submit']) {
this.dom['btn-submit'].disabled = true;
this.dom['btn-submit'].textContent = 'Proceed to Payment';
}
this.state.currentStep = 1;
this.state.isBusy = false;
this.state.leadId = null;
this.state.leadRef = null;
this.handleConditionalUI();
this.updateFeeTable();
this.goTo(1);
},
calcFees: function (isCC) {
var base = this.config.fee;
var sur = isCC ? this.round2(base * this.config.ccSurchargeRate) : 0;
var gst = this.round2((base + sur) * this.config.gstRate);
return { base: base, surcharge: sur, gst: gst, total: this.round2(base + sur + gst) };
},
updateFeeTable: function () {
var fees = this.calcFees(this.val('payment-method') === 'cc');
if (this.dom['surcharge-row']) this.dom['surcharge-row'].textContent = '$' + fees.surcharge.toFixed(2);
if (this.dom['gst-row']) this.dom['gst-row'].textContent = '$' + fees.gst.toFixed(2);
if (this.dom['total-row']) this.dom['total-row'].textContent = '$' + fees.total.toFixed(2);
},
showError: function (id) { var el = this.byId(id); if (el) el.classList.add('visible'); },
hideError: function (id) { var el = this.byId(id); if (el) el.classList.remove('visible'); },
focusFirstError: function () {
var err = document.querySelector('.error-msg.visible');
if (!err) return;
var grp = err.closest('.field-group');
if (!grp) return;
var inp = grp.querySelector('input, select, textarea');
if (inp) inp.focus();
},
/* Validation */
validateStep: function (step) {
var ok = true;
function req(id, errId) {
if (!App.val(id)) { App.showError(errId); ok = false; }
else { App.hideError(errId); }
}
if (step === 1) {
req('accred-status', 'err-accred');
var accred = this.val('accred-status');
if (accred === 'yes-other') { req('other-org-name', 'err-other-org-name'); req('other-expiry-date', 'err-other-expiry-date'); }
if (accred === 'no') { req('commencement-date', 'err-commencement-date'); }
}
if (step === 2) {
req('practice-name', 'err-practice-name'); req('practice-abn', 'err-practice-abn');
req('practice-phone', 'err-practice-phone'); req('practice-email', 'err-practice-email');
req('standards', 'err-standards');
}
if (step === 3) {
req('contact-first', 'err-contact-name'); req('contact-last', 'err-contact-name');
req('contact-position', 'err-contact-position'); req('contact-phone', 'err-contact-phone');
req('contact-email', 'err-contact-email'); req('gp-first', 'err-gp-name');
req('gp-last', 'err-gp-name');
}
if (step === 4) {
req('addr1', 'err-address'); req('city', 'err-address');
req('state', 'err-address'); req('postcode', 'err-address');
var postalYes = document.querySelector('input[name="postal"][value="yes"]:checked');
if (postalYes) {
req('postal-addr1', 'err-postal-address'); req('postal-city', 'err-postal-address');
req('postal-state', 'err-postal-address'); req('postal-postcode', 'err-postal-address');
} else { this.hideError('err-postal-address'); }
}
if (step === 5) {
req('phn', 'err-phn'); req('practice-type', 'err-practice-type'); req('group-accred', 'err-group-accred');
var ga = this.val('group-accred');
if (ga === 'yes-corporate' || ga === 'yes-group') { req('corporate-entity-name', 'err-corporate-entity'); }
else { this.hideError('err-corporate-entity'); }
}
if (step === 6) {
var pCount = this.val('practitioners');
if (!pCount) { this.showError('err-practitioners'); ok = false; }
else { this.hideError('err-practitioners'); }
if (pCount && pCount !== 'over-10') {
var n = parseInt(pCount, 10);
for (var i = 1; i <= n; i++) {
if (!this.val('prac-first-' + i) || !this.val('prac-last-' + i) || !this.val('prac-hours-' + i) || !this.val('prac-type-' + i)) {
this.showError('err-practitioner-rows'); ok = false; break;
} else { this.hideError('err-practitioner-rows'); }
}
}
}
/* Step 7 — payment method only required for non-pending registrations */
if (step === 7) {
if (!this.isPendingRegistration()) {
req('payment-method', 'err-payment');
} else {
this.hideError('err-payment');
}
}
if (!ok) this.focusFirstError();
return ok;
},
areDeclarationsChecked: function () {
var basicChecked = !!(
this.byId('cb-auth') && this.byId('cb-auth').checked &&
this.byId('cb-terms') && this.byId('cb-terms').checked &&
this.byId('cb-prof') && this.byId('cb-prof').checked
);
if (!basicChecked) return false;
/* For pending registrations the additional acknowledgment checkbox is mandatory */
if (this.isPendingRegistration()) {
return !!(this.byId('cb-pending-ack') && this.byId('cb-pending-ack').checked);
}
return true;
},
updateSubmitEnabled: function () {
if (!this.dom['btn-submit']) return;
/* Pending path: enabled purely by checkbox state — lead created on submit.
Normal path: also requires leadId to exist (set after page 7 creates the lead). */
var declarationsOk = this.areDeclarationsChecked();
var canSubmit = this.isPendingRegistration()
? declarationsOk && !this.state.isBusy
: declarationsOk && !this.state.isBusy && !!this.state.leadId;
this.dom['btn-submit'].disabled = !canSubmit;
},
/* Data collection */
collectPractitioners: function () {
var result = [];
var v = this.val('practitioners');
if (!v || v === 'over-10') return result;
var count = parseInt(v, 10);
for (var i = 1; i <= count; i++) {
var firstName = this.val('prac-first-' + i);
var lastName = this.val('prac-last-' + i);
result.push({
title: this.val('prac-title-' + i),
firstName: firstName,
lastName: lastName,
name: (firstName + ' ' + lastName).trim(),
hours: this.val('prac-hours-' + i),
type: this.val('prac-type-' + i)
});
}
return result;
},
collectFormData: function () {
var accredVal = this.val('accred-status');
var postalSameAsStreet = !document.querySelector('input[name="postal"][value="yes"]:checked');
return {
accredStatus: accredVal,
otherOrgName: accredVal === 'yes-other' ? this.val('other-org-name') : '',
otherExpiry: accredVal === 'yes-other' ? this.val('other-expiry-date') : '',
commenceDate: accredVal === 'no' ? this.val('commencement-date') : '',
isPending: this.isPendingRegistration(),
practiceName: this.val('practice-name'),
practiceAbn: this.val('practice-abn'),
practicePhone: this.val('practice-phone'),
practiceFax: this.val('practice-fax'),
practiceEmail: this.val('practice-email'),
standards: this.val('standards'),
contactPrefix: this.val('contact-prefix'),
contactFirst: this.val('contact-first'),
contactLast: this.val('contact-last'),
contactPosition: this.val('contact-position'),
contactPhone: this.val('contact-phone'),
contactEmail: this.val('contact-email'),
contactMethod: this.val('contact-method'),
gpPrefix: this.val('gp-prefix'), gpFirst: this.val('gp-first'), gpLast: this.val('gp-last'),
addr1: this.val('addr1'), addr2: this.val('addr2'), city: this.val('city'),
state: this.val('state'), postcode: this.val('postcode'),
postalSameAsStreet: postalSameAsStreet,
postalAddr1: postalSameAsStreet ? '' : this.val('postal-addr1'),
postalAddr2: postalSameAsStreet ? '' : this.val('postal-addr2'),
postalCity: postalSameAsStreet ? '' : this.val('postal-city'),
postalState: postalSameAsStreet ? '' : this.val('postal-state'),
postalPostcode: postalSameAsStreet ? '' : this.val('postal-postcode'),
phn: this.val('phn'), pipId: this.val('pip-id'),
practiceType: this.val('practice-type'), groupAccred: this.val('group-accred'),
corporateEntity: this.val('corporate-entity-name'),
practitionerCount: this.val('practitioners'),
practitioners: this.collectPractitioners(),
paymentMethod: this.val('payment-method')
};
},
/* Payloads */
buildLeadPayload: function (d) {
var isPracticeAccredited = d.accredStatus === 'yes-agpal' ? 107150001
: d.accredStatus === 'yes-other' ? 107150002 : 107150000;
var groupAccredValue = d.groupAccred === 'yes-corporate' ? 107150001
: d.groupAccred === 'yes-group' ? 107150002 : 107150000;
var practiceTypeGuid = this.config.practiceTypeGuids[d.practiceType] || null;
/* For pending registrations, flag the subject and status so
can route the notification to adminbd@agpal.com.au (BCC marketingteam@agpal.com.au)
and NOT progress into downstream systems Internally. */
var subjectLine = d.isPending
? 'AGPAL Registration (ON HOLD \u2013 Pending Commencement) \u2013 ' + d.contactFirst + ' ' + d.contactLast
: 'AGPAL Registration \u2013 ' + d.contactFirst + ' ' + d.contactLast;
var payload = {
subject: subjectLine,
firstname: d.contactFirst,
lastname: d.contactLast || d.practiceName,
jobtitle: d.contactPosition,
emailaddress1: d.contactEmail || d.practiceEmail,
telephone1: d.contactPhone,
mobilephone: d.practicePhone,
fax: d.practiceFax,
companyname: d.practiceName,
leadsourcecode: 8,
address1_line1: d.addr1,
address1_line2: d.addr2,
address1_city: d.city,
address1_stateorprovince: d.state,
address1_postalcode: d.postcode,
address1_country: 'Australia',
ongc_state: App.mapState(d.state), /* integer choice */
ongc_country: 107150000, /* Australia */
address2_line1: d.postalAddr1,
address2_line2: d.postalAddr2,
address2_city: d.postalCity,
address2_stateorprovince: d.postalState,
address2_postalcode: d.postalPostcode,
address2_country: d.postalAddr1 ? 'Australia' : '',
ongc_enquirytype: 8,
ongc_practicename: d.practiceName,
ongc_practiceabn: d.practiceAbn,
ongc_practicephone: d.practicePhone,
ongc_pipid: d.pipId,
ongc_corporateentity: d.corporateEntity,
ongc_groupname: d.corporateEntity,
ongc_postalsameasstreet: d.postalSameAsStreet,
ongc_ispracticeaccredited: isPracticeAccredited,
ongc_grouppracticesalreadyaccredited: groupAccredValue,
ongc_accreditationexpirydate: d.otherExpiry || null,
ongc_duedate: d.commenceDate || null,
ongc_operationalcommencementdate: d.commenceDate || null,
ongc_accreditationorganisationname: d.otherOrgName,
statuscode: d.isPending ? 107150002 : undefined,
ongc_webresponse: App.buildAgpalWebResponseHtml(d)
};
/*
* NOTE: ongc_PracticeType and ongc_Business odata.bind are NOT set here.
* Anonymous users lack Append To on both related tables so any odata.bind
* causes a 90040106 permission error.
*
* We also cannot add fake columns (ongc_practicetypeguid etc.) because
* Dataverse rejects unknown properties with a 9004010D error.
*
* Solution: encode both GUIDs as a small JSON object in the Lead's
* standard `description` field. The plugin reads it, sets both lookups
* as SYSTEM, then clears the description field.
*/
/*
* Encode lookup GUIDs + extra fields into description for the plugin.
* Keys:
* practiceTypeGuid — resolves ongc_PracticeType lookup (Append To blocked for anon)
* businessGuid — resolves ongc_Business lookup (same reason)
* corporateEntity — used as Account name fallback / ongc_entityname
* practiceEmail — emailaddress2 on Account
* practicePhone — address1_telephone1 on Account
*/
var pluginMeta = {};
if (practiceTypeGuid) pluginMeta.practiceTypeGuid = practiceTypeGuid;
if (App.config.businessId) pluginMeta.businessGuid = App.config.businessId;
if (d.corporateEntity) pluginMeta.corporateEntity = d.corporateEntity;
if (d.practiceEmail) pluginMeta.practiceEmail = d.practiceEmail;
if (d.practicePhone) pluginMeta.practicePhone = d.practicePhone;
if (d.practiceAbn) pluginMeta.practiceAbn = d.practiceAbn;
if (d.practiceName) pluginMeta.practiceName = d.practiceName;
/* Practitioners: created + N:N associated server-side by the plugin
(SYSTEM) from this same blob — see note above buildPractitionerPayloads.
Anonymous never touches the ongc_practitioner table directly. */
var pracPayloads = this.buildPractitionerPayloads(d);
if (pracPayloads.length) pluginMeta.practitioners = pracPayloads;
payload['description'] = '__PLUGIN_META__' + JSON.stringify(pluginMeta);
/*
* NOTE: parentaccountid and parentcontactid are intentionally NOT set here.
* Account and Contact lookup/creation and linking is handled server-side
* by the AgpalLeadCreatePlugin Dataverse plugin, which runs as SYSTEM.
* Anonymous portal users have NO permission to read, write, or query
* Account or Contact records.
*/
Object.keys(payload).forEach(function (k) {
if (payload[k] === '' || payload[k] === null || payload[k] === undefined) delete payload[k];
});
return payload;
},
/* Practitioners: anonymous never calls the ongc_practitioner API at all.
Every client-side approach ($ref associate, and POSTing to the N:N
nav property as a contained entity) hit the same Power Pages
EntityPermissionCreateIsMissing check on Lead, regardless of which
table permissions were granted — short of Read/Write/Append/AppendTo
on both tables (an exposure we're avoiding), there's no working
client-side path. So instead, practitioner rows are embedded in the
same __PLUGIN_META__ blob already used for ongc_PracticeType/
ongc_Business, and PortalLeadCreatePlugin (SYSTEM) creates each
Practitioner and associates it to the Lead during the same Lead
Create transaction. Anonymous only ever needs Create on Lead. */
buildPractitionerPayloads: function (d) {
var rows = d.practitioners || [];
return rows.map(function (prac) {
var hours = parseFloat(prac.hours);
var fullName = (prac.firstName + ' ' + prac.lastName).trim();
var payload = {
ongc_title: prac.title || '',
ongc_firstname: prac.firstName || '',
ongc_lastname: prac.lastName || '',
ongc_name: fullName || prac.name || '',
ongc_type: App.config.practitionerTypeMap[prac.type] !== undefined
? App.config.practitionerTypeMap[prac.type] : 10,
ongc_hoursperweek: isNaN(hours) ? null : hours
};
Object.keys(payload).forEach(function (k) {
if (k === 'ongc_type') return;
if (payload[k] === null || payload[k] === undefined || payload[k] === '') delete payload[k];
});
return payload;
});
},
/* Lead card */
renderLeadCard: function (d) {
if (!this.dom['lead-card-content']) return;
function row(label, value) {
return '
' + label + '' + (value || '\u2014') + '
';
}
this.dom['lead-card-content'].innerHTML =
row('Reference', App.state.leadRef || (App.state.leadId || '').toUpperCase()) +
row('Practice', d.practiceName) +
row('ABN', d.practiceAbn) +
row('Contact', (d.contactFirst + ' ' + d.contactLast).trim()) +
row('Email', d.contactEmail || d.practiceEmail) +
row('Phone', d.contactPhone) +
row('Payment', this.val('payment-method') === 'eft' ? 'Bank Transfer (EFT)' : 'Credit Card');
if (this.dom['lead-card']) this.dom['lead-card'].style.display = 'block';
if (this.dom['ref-number']) this.dom['ref-number'].textContent = App.state.leadRef || (App.state.leadId || '').toUpperCase();
},
/* Create the Lead. NOTE: ongc_leadrefid can NOT come back in this
response — Dataverse requires Read privilege on Lead to include any
field beyond the primary key via $select/return=representation,
proven across every approach tried (confirmed via a direct $select
test that returned EntityPermissionReadIsMissing). Anonymous only
has Create, by design, so this call only returns leadid. The
reference number is fetched separately via fetchLeadRefFromFlow. */
createLeadWithRef: async function (payload) {
var token = await this.waitForToken();
var resp = await fetch(
'/_api/leads',
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
'__RequestVerificationToken': token,
'OData-MaxVersion': '4.0',
'OData-Version': '4.0'
},
body: JSON.stringify(payload)
}
);
if (!resp.ok) {
var em = 'API error ' + resp.status;
try { var ej = await resp.json(); em = (ej && ej.error && ej.error.message) || em; } catch (e) { }
throw new Error(em);
}
var data = {};
try { data = await resp.json(); } catch (e) { }
return { id: this.extractEntityId(data, resp), data: data };
},
/* Look up ongc_leadrefid via the Power Automate HTTP-trigger flow —
the only channel that can return it without granting anonymous
Read on Lead (a deliberate constraint, not an oversight). Fails
soft: any error here just leaves leadRef null and the card shows
"Pending" — it never blocks or fails the registration itself. */
fetchLeadRefFromFlow: async function (leadId) {
if (!App.config.leadRefFlowUrl || !leadId) return null;
try {
var resp = await fetch(App.config.leadRefFlowUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ leadId: leadId })
});
if (!resp.ok) { console.warn('[AGPAL] leadRef flow HTTP', resp.status); return null; }
var data = await resp.json();
return (data && data.leadRef) || null;
} catch (e) {
console.warn('[AGPAL] leadRef flow error:', e.message);
return null;
}
},
createLeadAndChildren: async function () {
var d = this.collectFormData();
this.setBusy(true, 'Submitting registration...10% \u2026');
this.saveState();
try {
/* 1 — Lead + Practitioners (embedded in pluginMeta) + Account/Contact
linking — all handled server-side by the plugin in one Create. */
var lead = await this.createLeadWithRef(this.buildLeadPayload(d));
this.state.leadId = lead.id;
if (!this.state.leadId) throw new Error('Lead GUID not returned. Check API permissions.');
console.log('[AGPAL] Lead created:', this.state.leadId);
console.log('[AGPAL] Account/Contact/Practitioner linking delegated to server-side plugin.');
this.saveState();
/* ongc_leadrefid via the Flow lookup — see fetchLeadRefFromFlow.
Non-blocking: if this fails, leadRef stays null and the card
shows "Pending" rather than holding up the registration. */
this.state.leadRef = await this.fetchLeadRefFromFlow(this.state.leadId);
this.saveState();
/* Done */
if (d.isPending) {
/* Pending path: navigate to success page, render lead card there */
this.goToSuccess(true, d);
this.showToast('Registration submitted successfully.');
} else {
/* Normal path: show lead card on page 8, user proceeds to payment */
this.renderLeadCard(d);
if (this.dom['lead-status-msg']) {
this.dom['lead-status-msg'].style.display = 'block';
this.dom['lead-status-msg'].className = 'lead-status-ready';
this.dom['lead-status-msg'].textContent = 'Registration record submitted. You may now proceed to payment.';
}
this.goTo(8);
/* Re-apply pending UI now that we are on page 8 */
this.handleConditionalUI();
this.showToast('Registration submitted successfully.');
}
} catch (err) {
console.error('[AGPAL] Lead creation failed', err);
if (this.dom['lead-status-msg']) {
this.dom['lead-status-msg'].style.display = 'block';
this.dom['lead-status-msg'].className = 'lead-status-error';
this.dom['lead-status-msg'].textContent = 'Registration could not be submitted. Please try again.';
}
this.showToast(err.message || 'Registration creation failed.', true);
} finally {
this.setBusy(false);
this.updateSubmitEnabled();
}
},
/* Payment evidence */
createPaymentEvidence: async function (stripeResult) {
stripeResult = stripeResult || {};
var isCC = this.val('payment-method') === 'cc';
var fees = this.calcFees(isCC);
var now = new Date().toISOString();
/*
* DO NOT include ongc_Lead@odata.bind here.
* Anonymous portal users lack Append To on lead → ongc_paymentevidence,
* which causes error 90040106 on any @odata.bind that references lead.
*
* The Lead linkage is handled by AgpalPaymentEvidenceCreatePlugin:
* Entity: ongc_paymentevidence | Message: Create | Stage: Post-Op | Run As: System
*
* The leadId is carried in ongc_description (JSON) which already
* exists on this table. The plugin reads it and sets ongc_Lead as SYSTEM.
* No extra column or description field needed on ongc_paymentevidence.
*/
var leadId = this.state.leadId || '';
var payload = {
ongc_payername: stripeResult.fullName || (this.val('contact-first') + ' ' + this.val('contact-last')).trim(),
ongc_paymentamount: fees.base,
ongc_surchargefee: fees.surcharge,
ongc_gst: fees.gst,
ongc_totalamount: fees.total,
ongc_paymentreference: stripeResult.paymentIntentId || stripeResult.chargeId || '',
ongc_paymentstatus: stripeResult.status || (isCC ? 'succeeded' : 'pending-eft'),
/* Payment Mode (Choice): 107150000 = Credit Card, 107150001 = Bank Transfer */
ongc_paymentmode: isCC ? 107150000 : 107150001,
ongc_paymentdate: stripeResult.paymentDate || now,
/* Embed leadId in the response message JSON so the plugin can extract it.
__leadId__ is a known key the plugin looks for. */
ongc_description: JSON.stringify(
Object.assign({ __leadId__: leadId }, stripeResult.rawResponse || stripeResult || {}),
null, 2
)
};
Object.keys(payload).forEach(function (k) {
if (payload[k] === '' || payload[k] === null || payload[k] === undefined) delete payload[k];
});
console.log('[AGPAL] Payment Evidence payload (no odata.bind):', JSON.stringify(payload));
return this.dataversePost('ongc_paymentevidences', payload);
},
/* Submit */
handleSubmit: async function () {
if (!this.areDeclarationsChecked()) { this.showError('err-submit'); this.focusFirstError(); return; }
this.hideError('err-submit');
this.hideError('err-pending-ack');
/* Pending path: create lead record NOW (on page 8 "Confirm and Submit").
createLeadAndChildren detects isPending and routes to goToSuccess(true). */
if (this.isPendingRegistration()) {
await this.createLeadAndChildren();
return;
}
/* Normal path: lead already created on page 7.
Credit card -> Stripe payment. Bank transfer (EFT) -> no Stripe,
registration is finalised immediately. */
this.setBusy(true, 'Processing\u2026');
this.saveState();
try {
if (!this.state.leadId) {
throw new Error('Registration record not found. Please go back and recreate the registration.');
}
if (this.val('payment-method') === 'eft') {
await this.completeEftRegistration();
} else {
await this.initiateStripePayment();
}
} catch (err) {
console.error('[AGPAL] Submit failed', err);
this.showToast(err.message || 'Submission failed.', true);
this.setBusy(false);
this.updateSubmitEnabled();
}
},
/* Bank transfer (EFT): no Stripe session — create the payment evidence
record as pending-eft and complete the registration straight away.
Mirrors what agpalHandleStripeReturn does for the CC/Stripe path,
minus the redirect and Stripe-specific response data. */
completeEftRegistration: async function () {
this.setBusy(true, 'Finalising registration\u2026');
await this.createPaymentEvidence({ status: 'pending-eft' });
this.setBusy(true, 'Updating registration record\u2026');
await this.appendPaymentToWebResponse({ status: 'pending-eft' }, this.calcFees(false), 'Bank Transfer');
this.goToSuccess(false);
this.showToast('Registration submitted successfully.');
},
initiateStripePayment: function () {
var fees = this.calcFees(true);
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: this.val('contact-email') || this.val('practice-email'),
name: (this.val('contact-first') + ' ' + this.val('contact-last')).trim(),
ref: 'AGPAL Registration \u2013 ' + this.val('practice-name'),
returnUrl: window.location.href.split('?')[0]
});
window.location.href = '/agpal-stripe-checkout?' + params.toString();
},
/* Navigation */
goTo: function (step) {
document.querySelectorAll('.step-page').forEach(function (p) { p.classList.remove('active'); });
var page = this.byId('page-' + step);
if (page) page.classList.add('active');
document.querySelectorAll('.step-dot').forEach(function (d, i) {
d.classList.toggle('active', i + 1 === step);
d.classList.toggle('completed', i + 1 < step);
});
var titles = [
'Step 1: Accreditation Status', 'Step 2: Practice Information',
'Step 3: Practice Contacts', 'Step 4: Practice Address',
'Step 5: Further Practice Details', 'Step 6: Practitioner Details',
'Step 7: Self Assessment Fee', 'Step 8: Authorisation & Submission'
];
if (step <= this.state.totalSteps) {
if (this.dom.stepTitle) this.dom.stepTitle.textContent = titles[step - 1];
if (this.dom.stepCount) this.dom.stepCount.textContent = 'Page ' + step + ' of ' + this.state.totalSteps;
if (this.dom.progressFill) this.dom.progressFill.style.width = ((step / this.state.totalSteps) * 100) + '%';
}
this.state.currentStep = step;
window.scrollTo(0, 0);
},
goToSuccess: function (isPending, d) {
document.querySelectorAll('.step-page').forEach(function (p) { p.classList.remove('active'); });
var s = this.byId('page-success');
if (s) s.classList.add('active');
/* Show the appropriate success message depending on registration type */
var msgNormal = this.byId('success-msg-normal');
var msgPending = this.byId('success-msg-pending');
if (msgNormal) msgNormal.style.display = isPending ? 'none' : 'block';
if (msgPending) msgPending.style.display = isPending ? 'block' : 'none';
/* Reference number */
if (this.dom['ref-number']) {
this.dom['ref-number'].textContent = App.state.leadRef || (App.state.leadId || '').toUpperCase();
}
/* Pending path: render lead status + card inside the success page */
if (isPending && d) {
var statusEl = this.byId('pending-lead-status-msg');
if (statusEl) {
statusEl.textContent = 'Your details have been recorded. A member of the AGPAL Client Liaison team will contact you closer to your opening date to progress your registration.';
statusEl.style.display = 'block';
}
this.renderLeadCardPending(d);
}
this.setBusy(false);
window.scrollTo(0, 0);
},
/* Render lead card on the success page */
renderLeadCardPending: function (d) {
var container = this.byId('pending-lead-card-content');
if (!container) return;
function row(label, value) {
return '
' + label + '' + (value || '\u2014') + '
';
}
container.innerHTML =
row('REFERENCE', App.state.leadRef || (App.state.leadId || '').toUpperCase()) +
row('PRACTICE', d.practiceName) +
row('ABN', d.practiceAbn) +
row('CONTACT', (d.contactFirst + ' ' + d.contactLast).trim()) +
row('EMAIL', d.contactEmail) +
row('PHONE', d.contactPhone);
var card = this.byId('pending-lead-card');
if (card) card.style.display = 'block';
},
/* Conditional UI */
handleConditionalUI: function () {
var accred = this.val('accred-status');
var yesAgpal = this.byId('group-yes-agpal');
var yesOther = this.byId('group-yes-other');
var noGroup = this.byId('group-no');
var btnNext1 = this.byId('btn-next-1');
var btnSubmitAg = this.byId('btn-submit-agpal');
if (yesAgpal) yesAgpal.style.display = accred === 'yes-agpal' ? 'block' : 'none';
if (yesOther) yesOther.style.display = accred === 'yes-other' ? 'block' : 'none';
if (noGroup) noGroup.style.display = accred === 'no' ? 'block' : 'none';
if (btnNext1) btnNext1.style.display = accred === 'yes-agpal' ? 'none' : 'inline-block';
if (btnSubmitAg) btnSubmitAg.style.display = accred === 'yes-agpal' ? 'inline-block' : 'none';
var ga = this.val('group-accred');
var corpWrap = this.byId('group-corporate-entity');
if (corpWrap) corpWrap.style.display = (ga === 'yes-corporate' || ga === 'yes-group') ? 'block' : 'none';
var postalYes = document.querySelector('input[name="postal"][value="yes"]:checked');
var postalWrap = this.byId('postal-address-group');
if (postalWrap) postalWrap.style.display = postalYes ? 'block' : 'none';
var v = this.val('practitioners');
if (this.dom['practitioner-dynamic-section']) this.dom['practitioner-dynamic-section'].style.display = 'none';
if (this.dom['practitioner-over10-section']) this.dom['practitioner-over10-section'].style.display = 'none';
if (v === 'over-10') {
if (this.dom['practitioner-over10-section']) this.dom['practitioner-over10-section'].style.display = 'block';
this.state.practitionerRowCount = 0;
if (this.dom['practitioner-rows-container']) this.dom['practitioner-rows-container'].innerHTML = '';
} else if (v) {
var count = parseInt(v, 10);
if (!isNaN(count) && count >= 1 && count <= 10) {
this.buildPractitionerRows(count);
if (this.dom['practitioner-dynamic-section']) this.dom['practitioner-dynamic-section'].style.display = 'block';
}
}
/* Pending Registration UI — driven by isPendingRegistration() */
var isPending = this.isPendingRegistration();
/* Page 1 — pending notice */
var pendingNoticeP1 = this.byId('pending-notice-p1');
if (pendingNoticeP1) pendingNoticeP1.style.display = isPending ? 'block' : 'none';
/* Page 7 — fee info box (two versions) */
var feeInfoNormal = this.byId('fee-info-normal');
var feeInfoPending = this.byId('fee-info-pending');
if (feeInfoNormal) feeInfoNormal.style.display = isPending ? 'none' : 'block';
if (feeInfoPending) feeInfoPending.style.display = isPending ? 'block' : 'none';
/* Page 7 — payment method dropdown (hidden for pending) */
var paymentMethodGroup = this.byId('payment-method-group');
if (paymentMethodGroup) paymentMethodGroup.style.display = isPending ? 'none' : 'block';
/* Page 7 — "What happens next?" box (two versions) */
var nextStepsNormal = this.byId('next-steps-normal');
var nextStepsPending = this.byId('next-steps-pending');
if (nextStepsNormal) nextStepsNormal.style.display = isPending ? 'none' : 'block';
if (nextStepsPending) nextStepsPending.style.display = isPending ? 'block' : 'none';
/* Page 7 — Next button label */
var btnNext7 = this.byId('btn-next-7');
if (btnNext7) btnNext7.textContent = isPending ? 'Confirm and Next' : 'Next';
/* Page 8 — "Save and return" notice (only relevant if payment is happening) */
var saveReturnNotice = this.byId('save-return-notice');
if (saveReturnNotice) saveReturnNotice.style.display = isPending ? 'none' : 'block';
/* Page 8 — pending notice at top */
var pendingNoticeP8 = this.byId('pending-notice-p8');
if (pendingNoticeP8) pendingNoticeP8.style.display = isPending ? 'block' : 'none';
/* Page 8 — pending acknowledgment checkbox group */
var pendingAckGroup = this.byId('pending-ack-group');
if (pendingAckGroup) pendingAckGroup.style.display = isPending ? 'block' : 'none';
/* Page 8 — fee table (hidden for pending — no payment being made) */
var feeTableWrap = this.byId('fee-table-wrap');
if (feeTableWrap) feeTableWrap.style.display = isPending ? 'none' : 'block';
/* Page 8 — lead-card/status-msg wrap:
Normal path → visible (populated after page 7 Next creates the lead).
Pending path → hidden here; shown on the success page instead after Confirm and Submit. */
var p8LeadWrap = this.byId('p8-lead-wrap');
if (p8LeadWrap) p8LeadWrap.style.display = isPending ? 'none' : 'block';
/* Page 8 — submit button label and enabled state for pending
Pending: enabled by declarations alone (no lead pre-creation required).
Normal: enabled only after lead GUID confirmed (updateSubmitEnabled handles this). */
var submitBtn = this.dom['btn-submit'];
if (submitBtn) {
submitBtn.textContent = isPending ? 'Confirm and Submit' : 'Proceed to Payment';
if (isPending) {
submitBtn.disabled = !this.areDeclarationsChecked();
}
}
},
buildPractitionerRows: function (count) {
if (!this.dom['practitioner-rows-container']) return;
if (this.state.practitionerRowCount === count) return;
this.state.practitionerRowCount = count;
this.dom['practitioner-rows-container'].innerHTML = '';
for (var i = 1; i <= count; i++) {
var row = document.createElement('div');
row.className = 'practitioner-row';
row.style.cssText = 'display:grid;grid-template-columns:0.6fr 1fr 1fr 1fr 0.7fr;gap:12px;margin-bottom:20px;align-items:end;';
row.innerHTML =
'
' +
'' +
'' +
'
' +
'
' +
'' +
'' +
'
' +
'
' +
'' +
'' +
'
' +
'
' +
'' +
'' +
'
' +
'
' +
'' +
'' +
'
';
this.dom['practitioner-rows-container'].appendChild(row);
}
},
/* Event binding */
bindEvents: function () {
var self = this;
['accred-status', 'group-accred', 'practitioners', 'payment-method'].forEach(function (id) {
var el = self.byId(id);
if (!el) return;
el.addEventListener('change', function () {
self.handleConditionalUI(); self.updateFeeTable(); self.saveState();
});
});
/* Commencement date drives the pending-registration logic —
re-evaluate conditional UI whenever it changes */
var commenceDateEl = self.byId('commencement-date');
if (commenceDateEl) {
commenceDateEl.addEventListener('change', function () {
self.handleConditionalUI(); self.updateFeeTable(); self.saveState();
});
}
document.querySelectorAll('input[name="postal"]').forEach(function (el) {
el.addEventListener('change', function () { self.handleConditionalUI(); self.saveState(); });
});
[1, 2, 3, 4, 5, 6].forEach(function (step) {
var btn = self.byId('btn-next-' + step);
if (!btn) return;
btn.addEventListener('click', function () {
if (self.validateStep(step)) { self.saveState(); self.goTo(step + 1); }
});
});
var btnNext7 = this.byId('btn-next-7');
if (btnNext7) {
btnNext7.addEventListener('click', function () {
if (!self.validateStep(7)) return;
if (self.isPendingRegistration()) {
/* Pending path: page 7 just navigates to page 8.
Lead is created later on page 8 "Confirm and Submit". */
self.saveState();
self.goTo(8);
self.handleConditionalUI();
} else {
/* Normal path: create lead + practitioners, then navigate to page 8.
Account/Contact linking handled by server-side plugin. */
self.createLeadAndChildren();
}
});
}
[2, 3, 4, 5, 6, 7, 8].forEach(function (step) {
var btn = self.byId('btn-prev-' + step);
if (!btn) return;
btn.addEventListener('click', function () { if (!self.state.isBusy) self.goTo(step - 1); });
});
var submit = this.byId('btn-submit');
if (submit) submit.addEventListener('click', function () { self.handleSubmit(); });
/* Close button on success page — closes tab or falls back to portal home */
var btnClose = this.byId('btn-close-success');
if (btnClose) {
btnClose.addEventListener('click', function () {
try {
window.close();
/* window.close() silently fails if page wasn't opened by script;
fall back to home after a short delay */
setTimeout(function () {
window.location.href = window.location.origin + '/';
}, 300);
} catch (e) {
window.location.href = window.location.origin + '/';
}
});
}
['cb-auth', 'cb-terms', 'cb-prof', 'cb-pending-ack'].forEach(function (id) {
var cb = self.byId(id);
if (!cb) return;
cb.addEventListener('change', function () { self.updateSubmitEnabled(); self.saveState(); });
});
var btnSubmitAgpal = this.byId('btn-submit-agpal');
if (btnSubmitAgpal) {
btnSubmitAgpal.addEventListener('click', function () {
self.showToast('Thank you. Please call 1300 362 111 to re-accredit.');
});
}
document.querySelectorAll('input, select, textarea').forEach(function (el) {
el.addEventListener('change', function () { self.saveState(); });
});
},
/* ── Web response HTML ──────────────────────────────────── */
buildAgpalWebResponseHtml: function (d) {
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 = '
'
+ '
'
+ '
AGPAL Registration — Accreditation Scope
'
+ '
Submitted: ' + now + '
';
if (d.isPending) {
html += '
'
+ '⚠ PENDING REGISTRATION — Practice not yet operational. DO NOT progress into Site Admin. '
+ 'Notify adminbd@agpal.com.au (BCC marketingteam@agpal.com.au). Hold until practice opens.'
+ '
';
}
html += section('1. Accreditation Status');
var aL = { 'yes-agpal': 'Yes \u2014 currently accredited with AGPAL', 'yes-other': 'Yes \u2014 accredited with another organisation', 'no': 'No \u2014 not currently accredited' };
html += row('Status', aL[d.accredStatus] || d.accredStatus);
if (d.otherOrgName) html += row('Other Organisation', d.otherOrgName);
if (d.otherExpiry) html += row('Expiry Date', d.otherExpiry);
if (d.commenceDate) html += row('Desired Commencement Date', (function (iso) {
var p = iso.split('-'); return p.length === 3 ? p[2] + '-' + p[1] + '-' + p[0] : iso;
})(d.commenceDate));
if (d.standards) html += row('Standards', d.standards);
html += section('2. Practice Information');
html += row('Practice Name', d.practiceName); html += row('ABN', d.practiceAbn);
html += row('Phone', d.practicePhone); html += row('Fax', d.practiceFax);
html += row('Email', d.practiceEmail);
html += section('3. Practice Contacts');
html += row('Preferred Contact', [d.contactPrefix, d.contactFirst, d.contactLast].filter(Boolean).join(' '));
html += row('Position', d.contactPosition); html += row('Contact Phone', d.contactPhone);
html += row('Contact Email', d.contactEmail); html += row('Preferred Method', d.contactMethod);
html += row('Principal GP', [d.gpPrefix, d.gpFirst, d.gpLast].filter(Boolean).join(' '));
html += section('4. Practice Address');
html += row('Street Address', [d.addr1, d.addr2, d.city, d.state, d.postcode, 'Australia'].filter(Boolean).join(', '));
if (!d.postalSameAsStreet) {
html += row('Postal Address', [d.postalAddr1, d.postalAddr2, d.postalCity, d.postalState, d.postalPostcode, 'Australia'].filter(Boolean).join(', '));
} else {
html += row('Postal Address', 'Same as street address');
}
html += section('5. Further Practice Details');
html += row('PHN', d.phn); html += row('PIP ID', d.pipId);
var ptL = { gp: 'General Practice', ahs: 'After Hours Service', mds: 'Medical Deputising Service', aboriginal: 'Aboriginal Health Service' };
html += row('Practice Type', ptL[d.practiceType] || d.practiceType);
var gaL = { 'yes-corporate': 'Yes \u2014 Corporate Entity', 'yes-group': 'Yes \u2014 Group', 'no': 'No' };
html += row('Group Practices Accredited', gaL[d.groupAccred] || d.groupAccred);
if (d.corporateEntity) html += row('Corporate / Group Name', d.corporateEntity);
html += section('6. Practitioners');
html += row('Count', d.practitionerCount === 'over-10' ? 'Over 10' : (d.practitionerCount || '\u2014'));
if (d.practitioners && d.practitioners.length) {
d.practitioners.forEach(function (p, i) {
var displayName = [p.title, p.firstName, p.lastName].filter(Boolean).join(' ') || p.name || '\u2014';
html += '